代码审计自定义应用
代码审计自定义应用
在上一篇,我们从黑盒角度讨论了如何利用常见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) => {
usercount=count;
var sha256_password = crypto.createHash('sha256').update(password).digest('hex');
dbo.collection("users").insertOne({
_id:usercount+1,
username:username,
password:sha256_password
},function(err, result) {
if (err) {
throw err;
}
res.redirect('/')
db.close();
});
});
} 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 个小写字母,以及任何特殊字符。