大多数文章都为 php,java 等语言的代码审计,js 作为一种脚本语言,也同样存在审计的必要。

# 1. 密钥硬编码

const jwt = require('jsonwebtoken');
const secret = process.env.NODE_ENV === 'production' ? process.env.JWT_SECRET : 'secret';
const authService = () => {
...
};

第 2 行用到了 js 的三元运算符,它是一个简洁的条件判断语句

condition ? valueIfTrue : valueIfFalse

了解了三元运算符的用法之后,就很容易看出问题在哪:

secret 作为密钥,按理说应在服务器的配置信息里获取,但如果 NODE_ENV 不是 'production' ,则会使用 'secret' 作为默认密钥。这在开发过程中可能方便,但如果不小心在生产环境中忘记设置 NODE_ENVJWT_SECRET ,则会导致使用一个简单的固定值 'secret' ,密钥就被获取了。

# 2. SQL 注入

js 代码同样可能存在 sql 注入。

app.get('/api/v1/users/filter', (req, res) => {
var where = []
var query ="SELECT * FROM users WHERE ";
var data = []
for (var param in req.query) {
  where.push(param+"=?")
  data.push(req.query[param]);
}
query+=where.join(" AND ");
db.query(query, data, (err,rows) => {
  if(err) return res.send(JSON.stringify({}));
  return res.send(JSON.stringify(rows));
});  
});

第 6 行:

where.push(param+"=?")

即使使用了参数化查询,还是可以进行 sql 注入

/api/vq/user/filter?[INJECTION] = value

此时注入的是参数名,而不是参数值。
例如:把 param 设置成 1=1 --
此时 sql 语句为:SELECT * FROM users WHERE 1=1 -- ,将返回所有用户的数据

第 12 行:

return res.send(JSON.stringify(rows));

返回了全部数据,可能会暴露敏感信息

# 3. 只过滤一遍的正则匹配

const express = require('express')
const path = require('path')
const app = express()
app.get('/download', (req, res) => {
    const filename = req.query.filename.replace(/\/\.\./g,"")
    return res.sendFile(path.resolve(__dirname+'/'+filename))
});

正则表达式: /\/\.\./g ,看起来很乱,逐步分析一下就好多了

  • / ... /g :这是正则表达式的语法, g 表示全局匹配,即会查找字符串中的所有符合条件的部分。
  • \/ :匹配一个斜杠 / 。在正则表达式中,斜杠 / 是特殊字符,需要用反斜杠 \ 进行转义。
  • \. :匹配一个点 . 。在正则表达式中, . 表示任意单个字符,因此也需要用反斜杠 \ 转义为字面上的点。
  • \.\. :表示连续的两个点 ..

所以这段表达式的意思匹配字符串中的 "/.." 模式,而 ../ 通常我们用来进行目录穿越

我们还可以看到 /download,结合上面的正则,其实就是为了防止我们进行任意文件下载,但是这段代码只进行了一遍过滤,我们可以构造恶意 URL 绕过这个过滤。这种方式在其他攻击类型的绕过也很常见,如 SQL,文件上传等。

例如: ..//.. ,它会进行一次过滤,变成 ../ ,这样还是可以进行目录穿越的,所以

恶意 URL:test.com/download?filename=..//....//....//....//....//....//..../etc/passwd

# 4.SQL 二次注入

app.get('/api/v1/users', (req, res) => {
var query ="SELECT * FROM users WHERE id=?";
db.query(query, req.query.id, (err,rows) => {
   if(err) return res.send(JSON.stringify({}));
   if(rows.length === 0) return res.send(JSON.stringify({}));
   var q2 ="SELECT * FROM groups WHERE owner='"+rows[0].name+"'";
   db.query(q2, (err2, rows2) => {
    if(err2) return res.send(JSON.stringify({})); 
    if(rows2.length === 0) return reject("")
      
    return res.send(JSON.stringify(rows2));
  })
})
});

可以通过添加恶意用户名如 test' ,来进行 sql 注入(二次注入)

# 5.SSTI 漏洞

const https = require('https');
const express = require('express')
const ejs = require('ejs')
const app = express()
const port = 3000
app.set( "view engine", "ejs" );
app.get( "/", ( req, res ) => {
    var template = `<h1>${req.query.name}</h1>`
    return res.send( ejs.render(template))
} );
app.listen(port, () => console.log(`Listening on port ${port}!`))

主要原因是使用了用户输入( req.query.name )直接构造模板字符串,并传入 EJS 模板引擎渲染。导致攻击者在 name 参数中插入恶意代码,从而执行任意代码或访问服务器端的数据。

分析:

  • 用户输入未经验证: 代码直接将 req.query.name 注入到 template 变量中,形成 <h1>${req.query.name}</h1> ,并且通过 ejs.render(template) 渲染。在 EJS 模板中, <%= %><%- %> 会输出变量的内容,而 <%= %> 会将变量作为 HTML 文本输出, <%- %> 会将内容作为 HTML 输出(不转义 HTML 标签)。然而,由于这里是字符串插值,EJS 将会直接将 ${req.query.name} 解析为 JavaScript 代码。
  • 未使用模板转义${req.query.name} 并没有使用 <%= %> 这样的标签转义,从而允许直接注入的表达式执行。
  • 渲染动态内容: 因为 ejs.render(template) 直接渲染用户输入的内容,这允许用户通过传入恶意的表达式来执行任意的模板指令。

例如:

http://localhost/?name=<%= process.env %>

修复也比较简单:

app.get("/", (req, res) => {
    const name = req.query.name || "Guest"; // 默认值以防为空
    const template = `<h1><%= name %></h1>`; // 添加 & lt;%= %> 对 name 进行 HTML 转义,防止攻击
    return res.send(ejs.render(template, { name }));
});

这个请求会尝试在模板中执行 <%= process.env %> ,从而输出服务器的环境变量(如系统路径、密钥等),导致信息泄露。攻击者也可以尝试注入更复杂的表达式,例如调用系统命令或其他内部函数,具体取决于应用环境和模板引擎的特性

# 6. 日志注入

const express = require('express')
const app = express()
const port = 3000
app.get('/dangerous', (req, res) => {
  /* [...] */
  console.log("Access to dangerous function: "+req.ip);
  /* [...] */
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

日志记录时,直接将用户的 IP 地址输出到控制台,而没有进行任何验证或处理,这可能导致 日志注入 攻击。具体问题如下:

分析

  1. 日志注入: 如果攻击者能够控制 req.ip (如用 X-Forwarded-For 伪造),可以通过构造恶意的请求,使得日志内容被注入任意数据。攻击者可以通过在 IP 地址中添加特殊字符或控制字符,甚至包含伪造的日志内容,来干扰或伪造日志记录,导致日志误导或注入恶意代码;如果知道日志文件的位置,还可以结合日志文件

  2. 伪造多行日志: 攻击者可以构造恶意的 IP 地址或使用代理来伪造多行日志,插入换行符或其他控制字符,制造混乱。例如:

    console.log("Access to dangerous function: " + req.ip);

    如果 req.ip 被伪造为 127.0.0.1\nMalicious log entry\nAnother log entry , 这样日志会显示为:

    Access to dangerous function: 127.0.0.1
    Malicious log entry
    Another log entry

    可能掩盖真正的日志信息,导致日志文件难以审计和分析。

# 后记

对于 JavaScript,还有很多利用方法,比如敏感数据泄露,数据加解密,路径泄露等,后续有动力再总结吧。

更新于 阅读次数