前 言
一次偶然的机会,让自己成为了一名CTF夺旗小白。从16年开始参与国内大中小型CTF竞赛,曾记得17年之前很少有人把这类竞赛叫做CTF,都是叫网络攻防赛、信息安全赛之类的,目的就是为了通过各种技术手段找到题目最终的key,现在我们都称之为Flag。
做为CTF小白用户,这四年跳过的坑真心不少,从最初的“实验吧”等免费的线上模拟平台到省级网络安全技能大赛,再到国内知名的XCTF联赛、DEFCON CTF,基本是跳一个栽一个。回首CTF的发展历程,也从最基本的解题模式发展成为如今的解题模式(Jeopardy)、攻防模式(Attack-Defense)、混合模式(Mix)三者于一体的网络安全竞赛,涵盖题型仍然是MISC、PPC、CRYPTO、PWN、REVERSE、WEB、STEGA等。
从题目的难易程度来看,题目难度不断加大,无论是MISC、CRYPTO、PWN、REVERSE还是WEB,都不再是那种用工具打开随便看看就能得到flag的题目了,几乎每个题目都会涉及到编写脚本来解题,而人生苦短我相信绝大多数人还是会选择Python。
从题型分布来看,PWN和REVERSE题目是每场CTF比赛的重头戏,也就是说只要PWN和REVERSE做的好,不进线下赛都困难~~,而Web题型也从原始的PHP考点向Python转型,从2018年开始,陆续出现了很多Python题目,如:18年护网杯的easy_tonado、强网杯的Python is the best language、TWCTF的Shrine、19年的SCTF也出了Ruby ERB SSTI的考点。
因此,本文就CTF中常见的Python考点进行了学习与汇总,在此也感谢各种大佬在自己博客中的辛勤付出,由于CTF题目在复现过程中出现了各种各样的问题,某些地方也直接照搬了大佬的博客。
一、CTF-pyc考点
1.1 pyc文件简介
pyc文件是py文件编译后生成的字节码文件(byte code),pyc文件经过python解释器最终会生成机器码运行。因此pyc文件是可以跨平台部署的,类似Java的.class文件,一般py文件改变后,都会重新生成pyc文件。
1.2 pyc文件生成方式
方法一:
在同一目录下创建两个py文件:test1.py 和 test2.py
test1.py文件内容:def a():
return 1
def b():
return 2
x = a()
y = b()
print(x + y)
test2.py文件内容:import test1
运行python test2.py生成test1.pyc文件。
方法二:python -m test1.py
直接运行以上命令也可以生成.pyc文件
方法三:
借助py_compile库和py_compile.compile('foo.py')方法
如果针对一个目录下所有的py文件进行编译,Python提供了一个模块叫compileall,具体请看下面代码:import compileall
compileall.compile_dir(r'/path')
1.3 pyc文件结构
接下来我们简单分析一下 pyc 文件结构,使用010 editor打开.pyc文件。
pyc文件一般由3个部分组成:最开始4个字节是一个Maigc int, 标识此pyc的版本信息, 不同的版本的Magic都在Python/import.c内定义
接下来四个字节还是个int,是pyc产生的时间(TIMESTAMP, 1970.01.01到产生pyc时候的秒数)
接下来是个序列化了的 PyCodeObject(此结构在Include/code.h内定义),序列化方法在Python/marshal.c内定义
1.4 CTF案例:PYTHON 字节码
python逆向基础资源:
crackme.pyc反编译后的内容如下:#!/usr/bin/env python
# encoding: utf-8
# 如果觉得不错,可以推荐给你的朋友!http://tool.lu/pyc
def encrypt(key, seed, string):
rst = []
for v in string:
rst.append((ord(v) + seed ^ ord(key[seed])) % 255)
seed = (seed + 1) % len(key)
return rst
if __name__ == '__main__':
print "Welcome to idf's python crackme"
flag = input('Enter the Flag: ')
KEY1 = 'Maybe you are good at decryptint Byte Code, have a try!'
KEY2 = [
124,
48,
52,
59,
164,
50,
37,
62,
67,
52,
48,
6,
1,
122,
3,
22,
72,
1,
1,
14,
46,
27,
232]
en_out = encrypt(KEY1, 5, flag)
if KEY2 == en_out:
print 'You Win'
else:
print 'Try Again !'
def encrypt(key, seed, string):
rst = []
for v in string:
rst.append((ord(v) + seed ^ ord(key[seed])) % 255)
seed = (seed + 1) % len(key)
return rst
if __name__ == '__main__':
print "Welcome to idf's python crackme"
flag = input('Enter the Flag: ')
KEY1 = 'Maybe you are good at decryptint Byte Code, have a try!'
KEY2 = [
124,
48,
52,
59,
164,
50,
37,
62,
67,
52,
48,
6,
1,
122,
3,
22,
72,
1,
1,
14,
46,
27,
232]
en_out = encrypt(KEY1, 5, flag)
if KEY2 == en_out:
print 'You Win'
else:
print 'Try Again !'
从程序看,KEY2内的整数似乎像ascii数值,但数字和英文字符少,直接转换意义不大。关键在于分析encrypt(KEY1, 5, flag)。
对encrypt函数的分析:用户输入一个字符串(ascii值必小于128),然后取出每个字符求其ascii值,加上seed,然后用其和与KEY1中一字符的ascii进行异或(算符\^,注意+比^的优先级高),然后对255求余。
编写解密程序。显然正确的密码字符串加密后结果为KEY2,那么逆向分析编码即可。程序如下:#python script
KEY2 = [124,
48,
52,
59,
164,
50,
37,
62,
67,
52,
48,
6,
1,
122,
3,
22,
72,
1,
1,
14,
46,
27,
232]
KEY1 = 'Maybe you are good at decryptint Byte Code, have a try!'
def encrypt(key, seed, string):
rst = []
for v in string:
rst.append((ord(v) + seed ^ ord(key[seed])) % 255)
seed = (seed + 1) % len(key)
return rst
def decrypt(key,seed,en_out ):
string = ''
for i in en_out :
v = (i ^ ord(key[seed]))-seed
seed = (seed + 1) % len(key)
if v > 0:
string += chr(v)
return string
if __name__ == '__main__':
print decrypt(KEY1,5,KEY2)
得到flag:WCTF{ILOVEPYTHONSOMUCH}
二、Python序列化和反序列化
2.1 序列化:把一个类对象转化为字节流(1)从对象提取所有属性,并将属性转化为名值对
(2)写入对象的类名
(3)写入名值对
在python中,一般可以使用pickle类来进行python对象的序列化,而cPickle提供了一个更快速简单的接口。
cPickle可以对任意一种类型的python对象进行序列化操作,比如list,dict,甚至是一个类的对象等。而所谓的序列化,可理解就是为了能够完整的保存并能够完全可逆的恢复。
1、dump: 将python对象序列化保存到本地的文件import cPickle
data = range(1000)
cPickle.dump(data,open("test\\data.pkl","wb"))
dump函数需要指定两个参数,第一个是需要序列化的python对象名称,第二个是本地的文件,需要注意的是,在这里需要使用open函数打开一个文件,并指定“写”操作
2、load:载入本地文件,恢复python对象data = cPickle.load(open("test\\data.pkl","rb"))
3、dumps:将python对象序列化保存到一个字符串变量中data_string = cPickle.dumps(data)
2.2 反序列化:将字节流转化为原始对象
(1)获取 pickle 输入流
(2)重建属性列表
(3)根据类名创建一个新的对象
(4)将属性复制到新的对象中
1、load:载入本地文件,恢复python对象data = cPickle.load(open("test\\data.pkl","rb"))
2、loads:从字符串变量中载入python对象data = cPickle.loads(data_string)
Python 的序列化的目的是为了保存、传递和恢复对象的方便性,在众多传递对象的方式中,序列化和反序列化可以说是最简单和最容易的方式。
Python序列化实例#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
def __reduce__(self):
return (os.system,('ls',))
a=test()
payload=pickle.dumps(a)
print payload
pickle.loads(payload)
对应的格式如下:c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中。
(:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组。
t:从堆栈中弹出对象,直到一个“(”被弹出,并创建一个包含弹出对象(除了“(”)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。
S:读取引号中的字符串直到换行符处,然后将它压入堆栈。
R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。
.:结束pickle。
2.3 Python反序列化漏洞
成因:当传入了不安全的反序列化函数的内容,就会产生反序列化漏洞,造成任意代码执行。
通常出现在解析认证token,session的时,flask配合redis在服务端存储session的情景。
这里的session是被pickle序列化进行存储的,如果你通过cookie进行请求session-id的话,session中的内容就会被反序列化,看似好像是没有什么问题。因为session是存储在服务端的,但是终究是抵不住redis的未授权访问,如果出现未授权的话,我们就能通过set设置自己的session,然后通过设置cookie去请求session的过程中我们自定的内容就会被反序列化,然后我们就达到了执行任意命令或者任意代码的目的。
2.4 Python unpickle 造成任意命令执行漏洞
web端源码:import pickle
import base6