Chall
I
思路:
此题是有特定版本的nodejs,当用一个字符串调用Buffer时,它将创建一个Buffer包含这些字节。但是如果它用一个数字调用,NodeJS将分配一个随机字节的大缓冲区。这是因为Buffer(number)不会使内存为零,并且它可能泄漏以前在堆上分配的数据。bodyParser.json(),发送包含数字的POST数据。返回一些从堆泄漏的内存。从而泄露出此题的第一个flag,但这中间加入了一些trick,就是输入的password是经过MD5,所以必须是纯数字的MD5,如下:
ximaz : 61529519452809720693702583126814
aalbke : 55203129974456751211900188750366
afnnsd : 49716523209578759475317816476053
aooalg : 68619150135523129199070648991237
bzbkme : 69805916917525281143075153085385
当然就有纯字符的md5了:
cbaabcdljdac : cadbfdfecdcdcdacdbbbfadbcccefabd
Chall II
思路:
Curl Post 数据,leak出的上一个flag,且给出部分源码,这里可以通过0lddriver搜github,填入leak出的flag作为config_session.keys,hmac后的cookies提交,获取admin权限,得到flag。
Blog
思路:
注册普通用户,在注册页面和修改密码页面可以提升权限为admin,在提升为admin后,可以在user页面看到flag,如果没有普通用户存在则无法查看flag。在注册页面和修改密码页面的表单中添加一个
<input type="text" name="user[admin]" value="1" />
提交后在删除用户的页面看到flag。
Text wall
思路:漏洞是在cookie中,首先将对象序列化并且sha1加密,且与序列化后进行连接urlencode后设置为cookie,突破点是首先反序列化读取index.php, 得到flag位置,再反序列化读取flag
Get Flag
思路:
采用黑名单的方式过滤了一些危险符号,但是没有过滤&或者%0a,可以使用其绕过黑名单,实现命令注入。
poc:flag=%0a+ls+../../9iZM2qTEmq67SOdJp%!oJm2%M4!nhS_thi5_flag&submit=
Pictures’ Wall
思路:
修改http包头中的host字段为127.0.0.1,成为root账户,登录页和注入无关,为了扰乱解思路,后台抓包修改上传图片为.phtml格式,执行.phtml文件读取flag,这里猥琐的点是大家可以相互删shell,互相搅屎。
Wallet
思路:将经过phpjm后的admin.php压缩为www.zip,设置密码为njctf2017,让选手爆破,得到源码解密后,审计源码,使cookie中的auth,md5(auth)与sha1(hsh)要相等,然后就是很普通SQLI(sqlite)了。
Sqlite injection tricks:https://github.com/unicornsasfuel/sqlite_sqli_cheat_sheet
Login
思路:注册admin(50+个空格)1用户,在插入数据库时,由于varchar()的长度限制,和mysql的默认配置,只会插入admin加一些空格,当mysql再次把它带入=运算进行字符串比较时,将会忽略所有末尾的空格,从而以admin身份查看flag。
Come on
思路:布尔型盲注。设定/*、*/、and、or、mid、substr、union、>、<、空白符、ascii等为敏感词,若检测到敏感词存在则重新跳转至index.php。参数使用mysql_real_escape_string()进行转义,但存在宽字节注入,可使用括号代替空格、使用left/right函数结合以代替substr()和mid()的方式进行注入,且此查询默认不区分大小写,需要使用binary(),最终payload形如:
%df%27||binary((right(left((select(flag)from(flag)),1),1))=hex值)
这样即可按位获取flag表中的flag内容。
Guess
思路:存在rfi漏洞可以直接读取题目的源码,通过设置自己的PHPSESSION为空,再解md5获取php_srand()的随机种子,即可预测出文件名,之后使用zip流来包含所上传的文件getshell,之后就可以获取flag。
Be Admin
思路:存在bak源码泄露以提供白盒,使用sqli控制查询语句的返回值,从而可以控制被传递进login函数的参数,由于php==运算符的安全问题,登陆验证函数可以被作为Oracle来使用,从而可以编写脚本,在没有密钥的情况下,通过padding oracle解出明文,再通过异或构造iv,从而在密文不变的情况下使解密后的结果变为admin,进而获取flag。
Be Logical
思路:给出一个业务系统,选手需要用里面的积分去换取金钱并购买增值服务,在退款的地方,选手可以通过输入point为1e3这样的数据来绕过检查,从而在退款中得到更多的分数。或者本题也可以获取.Sign.php.swp文件,能够发现签名算法存在缺陷,可以爆破secret_key长度并使用哈希长度扩展攻击伪造签名来进行刷钱;刷到相应的金额后可购买增值服务,即一个图片处理功能,其中存在ImageMagic RCE,通过恶意图片反弹shell后扫描内网,发现内网中存在phpmailer的邮件站点,并发现存在uploads目录,且很可能可写,直接用curl提交payload,写上去一个shell,在phpmailer主机上面获得flag。
Echo server
思路: 程序很短就是输入一个字符串然后返回该字符串的ascii编码. Strings分析后发现输入”F1@gA”后会有hint. 但代码打了一些花指令, 会使反汇编器混淆. 需要解题人对照opcode区分出 .text 中的数据和指令, patch掉垃圾代码. 还原出程序后可以看到flag就是”1@gA”的MD5值.
On the fly
思路: 该题在编译的过程中就对flag加了密, 需要通过逆向加密逻辑反推出解密.
first
思路:
将输入分为6份,每份由一个线程运行计算md5,比较相同加入mid[],线程运行顺序是随机的所以经过重新的排序。需要爆破找到满足条件的结果即flag,可以自己写程序爆破也可以把输入喂到程序里(不过这样成功率不高)。
vsvs
思路:系统本来可以执行system命令,但是被过滤了可以命令注入的字符。但是在read的时候存在栈溢出可以控制后面system将要执行的字符从而实现命令执行。
Pingme
思路:不提供源程序,blind formatstring攻击。
Random
思路: 程序每次猜单双的游戏连续取胜可以获得更长的数组访问,如果连续取胜数十次可以数组越界写造成栈溢出。猜单双的随机数是一个自己写的伪随机数生成器,可以通过计算初始状态预测之后的状态。(然而出题人留的栈长度不够。。。被爆破了Orz)
地球是平的
描述提示不是ECB,猜测是CBC加密,并且IV是Irving这个单词。观察密文特征。
注意CBC加密方式中是要前一密文段异或一次明文再加密一次的,这里出现了几乎一样的密文段,说明不存在复杂机密模式中的雪崩效应。仔细观察,标记里的两段密文仅某一位进行了反转,其余位都一样,在明文存在重复的情况下密文仅出现几个bit的差异,加密可能是对bit进行的异或运算。
一直向量长度为6byte,所以key和分组长度为6,解密的下一步就是求出key,求出key必须知道明文,这里我们只能对明文进行猜测,猜测几百jb大小的密文是某种文件,文件的文件头是固定的。多次尝试后猜测是gif文件,文件头为GIF89a,进行已知明文攻击,解密脚本:
import sys
data = list(open(sys.argv[1], 'rb').read())
block_size = 6
iv = 'Irving'
magic = 'GIF89a'
key = []
# compute the key from the first block
for i in range(0, block_size):
key.append(chr(ord(iv[i]) ^ ord(data[i]) ^ ord(magic[i])))
# decrypt it from end to start, easier to do since it's in place decryption
# make sure idx is block_size aligned
idx = len(data) - (len(data)%block_size) - block_size
while idx >= block_size:
for i in range(0, block_size):
data[idx+i] = chr(ord(data[idx+i])^ord(data[idx-block_size+i])^ord(key[i]))
idx -= block_size
for i in range(0, block_size):
data[i] = chr(ord(data[i])^ord(iv[i])^ord(key[i]))
open(sys.argv[2], 'wb').write(''.join(data))
得到解密后的gif文件仍然无法打开,查看gif的16进制信息:
大部分信息成功得到解密,但是观察发现每个分组的第4个和第5个字节解密失败,文件尾的所有第4个字节和第5个字节分别为8、9的却解密成功,猜想8和9的关系,猜测加密时先将第5个字节-1再与第4个字节加密后调换再进行异或加密,至此,写出解密脚本:
import sys
data = list(open(sys.argv[1], 'rb').read())
block_size = 6
iv = 'Irving'
magic = 'GIF89a'
key = []
# compute the key from the first block
for i in range(0, block_size):
key.append(chr(ord(iv[i]) ^ ord(data[i]) ^ ord(magic[i])))
# decrypt it from end to start, easier to do since it's in place decryption
# make sure idx is block_size aligned
idx = len(data) - (len(data) % block_size) - block_size
while idx >= block_size:
for i in range(0, block_size):
data[idx+i] = chr(ord(data[idx+i])^ord(data[idx-block_size+i])^ord(key[i]))
tmp = data[idx + 3]
data[idx + 3] = chr((ord(data[idx + 4]) - 1) % 256)
data[idx + 4] = chr((ord(tmp) + 1) % 256)
idx -= block_size
# idx = 0
# tmp = data[idx + 3]
# data[idx + 3] = chr((ord(data[idx + 4]) - 1) % 256)
# data[idx + 4] = chr((ord(tmp) + 1) % 256)
for i in range(0, block_size):
data[i] = chr(ord(data[i])^ord(iv[i])^ord(key[i]))
open(sys.argv[2], 'wb').write(''.join(data))
得到解密后的gif图片:
messager
思路:栈溢出,有canary,Brute-force stack canary byte by byte。不使用socat,利用socket直接跟选手进行通信。题目中可以设置一个getflag函数,从而使得选手一旦控制流劫持就可以拿到flag。
因为题目是直接用socket通信的,所以劫持控制流到/bin/sh是无法拿到shell的,而且不能通过stdio进行leak,要想拿shell只能开一个shell bind到一个端口。所以可以通过去掉getflag函数提高题目难度,从而考察选手是否会写bind shell的exp,if do,题目难度可以调整为中。
233
思路:
考察知识点:Vdso gadget SROP
#include<stdio.h>
void main(){
char c[10];
read(0,c,1024);
atoi(c);
}
程序本身只有这么多,除了canary 所有安全机制全开。
CANARY : disabled
FORTIFY : ENABLED
NX : ENABLED
PIE : ENABLED
RELRO : FULL
只能通过vdso的gadget做rop来拿shell
syscallhelper
思路:传统pwn+chroot 沙箱逃逸。
Pwn部分:
可以越界向堆中写堆地址,考虑改写vtable为内容为0x0c0c0c0c的堆地址,然后堆喷拿控制流之后逃逸沙箱
沙箱逃逸:chroot后的根目录不可写。只能在劫持控制流之后通过ptrace父进程来逃逸chroot沙箱
Easy_crypto
根据已有的加密源代码和已知明文及对应的密文,写出脚本对加密用的密钥key按位进行穷举。
得到key后根据加密脚本写出对应的解密脚本即可对flag进行解密
Key求解程序:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc != 3) {
printf("USAGE: %s plain_file cipher_file\n", argv[0]);
return 0;
}
FILE* plain_file = fopen(argv[1], "rb");
FILE* cipher_file = fopen(argv[2], "rb");
if (!plain_file || !cipher_file) {
printf("Error\n");
return 0;
}
char key[] = "XXXXXXXXXXXX";
char p, t, c, ch;
unsigned int j = 0;
int i = 0;
while ((p = fgetc(plain_file)) != EOF) {
c = fgetc(cipher_file);
// printf("read %d", p);
for (j=31;j<125;j++) {
ch = ((j ^ t) + (p-t) + i*i ) & 0xff;
if (ch == c) {
printf("%c\n",j);
t = p;
i++;
break;
}
}
}
return 0;
}
解密程序:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc != 3) {
printf("USAGE: %s input_file output_file\n", argv[0]);
return 0;
}
FILE* input_file = fopen(argv[1], "rb");
FILE* output_file = fopen(argv[2], "wb");
if (!input_file || !output_file) {
printf("Error\n");
return 0;
}
char key[] = "OKIWILLLETYOUKNOWWHATTHEKEYIS";
char p, t, c = 0;
int i = 0;
while ((c = fgetc(input_file)) != EOF) {
p = ( (c - (key[i % strlen(key)] ^ t) - i*i ) + t) & 0xff;
t = p;
i++;
fputc(p, output_file);
}
return 0;
}
knock Writeup
Knock.txt文件是用来将text.txt字母分割的文件,下划线代表空格,.代表字母,分割后的密文是一个单字母替换加密密文。
zjqz hexjz mo oqrs sai daiyn lebn zjo vos ltah zjer horrqxo e iron lobdo za voou zjo vos qfqs ltah mqn qrr joto er zjo horrqxo ebooqydrztyqqojolx
替换表:
得到key:ineealcstrlaaehefg
使用栅栏密码六个一组解密后得到
flag:ica nse eth ere alf lag
shooter Writeup
查看图片hex信息,发现在jpg后在0x24ef9处跟了一个去了头和尾的png文件,提取出来加上文件头后是一个二维码,扫出来“key 是‘boomboom’”,用outguess提取jpg图片的隐写信息:
traffic Writeup
查看pcap流量后,发现存在小部分IRC数据报文,把这些包过滤出来后对最后一个包进行tcp流追踪,得到完整的IRC会话。
会话中一方发送了一串奇怪的字符串并得到了一个get的回应,开始进行不断的base64字符串的发送,对前几行进行base64解码后发现是济慈的海伯利安的诗句,尝试解密最后一行并对解密后的明文自行再base64加密,发现同一明文的base64密文间存在区别:
存在base64隐写,并且只隐写了倒数第二个字节的最后两个bit。
解题脚本:
def get_base64_diff_value(s1, s2):
base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
res = 0
for i in xrange(len(s2)):
if s1[i] != s2[i]:
return abs(base64chars.index(s1[i]) - base64chars.index(s2[i]))
return res
def solve_stego():
with open('text.txt', 'rb') as f:
file_lines = f.readlines()
bin_str = ''
for line in file_lines:
steg_line = line.replace('\n', '')
norm_line = line.replace('\n', '').decode('base64').encode('base64').replace('\n','')
diff = get_base64_diff_value(steg_line, norm_line)
pads_num = steg_line.count('=')
if pads_num == 2:
pads_num = 1
if diff:
bin_str += bin(diff)[2:].zfill(pads_num * 2)
else:
bin_str += '0' * pads_num * 2
bin_str = bin_str[2:]
res_str = ''
for i in xrange(0, len(bin_str), 8):
res_str += chr(int(bin_str[i:i + 8], 2))
print res_str
solve_stego()
得到flag:NJCTF{Th4res_Ev1l_iN_1rC}
Ransom Writeup
首先使用DiskGenius对vmdk文件进行读取,
在xp-000001.vmdk的桌面存在4个文件,其中3个被加密,打开html文件
提示文件是被256位AES加密,并且提到如果30天内没有付款就会删除所有密钥,而日期是1月3日,已经过了30天,已经有文件被删除了。尝试使用DiskGenius进行文件恢复。
在桌面下成功恢复一个.key.zip压缩文件,打开后是三个aes密钥,但显然被加密了,加密前应该是32位。再看vmem文件,文件修改日期就是1月3日,当天内存是否会留存有密钥信息?尝试去分析vmem文件对密钥进行解密。
使用volatility查看vmem进程信息:
存在一个Winrar的进程,这个进程是否打开了某个带有密钥信息的文件?使用volatility的procdump功能将winrar进程信息dump出来。
对提取出来的文件进行strings提取。
仔细搜索提取出来的strings,找到了一个download.zip中的文件,recieve.txt,出现了一个32位的奇怪字符串,下面提到了private.jpg和public.jpg,这两个jpg就是rsa的私钥和公钥,再去vmdk中找(应该也被删除,需要恢复)。(这个txt文件真的是怕坑到人才放上去的,好像多虑了,导致降低了一些复杂度)
在用户根目录下找到这两个文件,打开后发现被加密过,根据勒索软件的行为,要么是rsa加密或者aes加密或者其他,想起当时那个32位的字符串,将其作为密钥尝试AES256解密文件,解密成功!在用rsa私钥解密三个AES密钥,再AES解密三个文本文件,拼接起来即可得到flag。
NJCTF{L3t_Vs_G0ooo0000_g000000_9o}
easycrack
Java层和native层的逆向与调试,题中有三个地方涉及到加密。没有加壳和混淆。
A.逆向的做法。看起来2处需要逆向,1处需要分析,其中1处加密使用原函数即可解密。所以总体来说需要逆向的只有一个简单的异或加密。
B.HOOK。因加密输出不受输入的前面字节影响,所以可以逐位暴破,但需要HOOK入输入的入口。
SafeBox
要逆向看出来一个隐藏的activity,需要am startactivity调出来。然后逆向出逻辑,得到正确的输入,输入正确解密数字之后,得到flag。
Littlerotategame
逆向混淆之后的arm代码。