强网杯2021-[强网先锋]协议 Writeup

robots

 

PPTP (RFC2637)

PPTP(Point to Point Tunneling Protocol),即点对点隧道协议,是在PPP协议的基础上开发的一种新的增强型安全协议, 支持多协议虚拟专用网(VPN),可以通过密码验证协议(PAP)、可扩展认证协议(EAP)等方法增强安全性。可以使远程用户通过拨入ISP、通过直接连接Internet或其他网络安全地访问企业网。

创建基于 PPTP 的 VPN 连接过程中,使用的认证机制与创建 PPP 连接时相同。此类认证机制主要有:扩展身份认证协议 (EAP,Extensible Authentication Protocol)、询问握手认证协议(CHAP,Challenge Handshake Authentication Protocol)和口令认证协议(PAP,Password Authentication Protocol),当前采用最多的是 CHAP 协议。PPTP用到的数据流量加密协议是MPPE。

 

CHAP (RFC 2433 和 RFC 2759)

CHAP 是基于挑战-响应的认证协议。挑战响应协议中,通常是验证者随机选择一个数作为挑战,声称者利用秘密信息和挑战生成响应,验证者根据验证响应是否正确来判定认证是否通过。

在 Windows 系统中PPTP协议实现采用的Microsoft版本的CHAP协议,目前有CHAP v1 和CHAP v2两个版本,分别在RFC 2433和RFC 2759中定义。

CHAP v1 协议流程如下:
(1) 客户端向服务器发送一个连接请求;
(2) 服务器返回一个 8 字节的随机挑战值Challenge;
(3) 客户端使用LAN Manager杂凑算法对用户口令做杂凑得到16字节输出,在其后补5个字节0得到21个字节值,按顺序分割为3个7字节值k1,k2,k3;
(4) 分别以k1,k2,k3为密钥对Challenge做DES加密,然后将三个密文块连接为一个24字节的响应;
(5) 客户端使用NTLM v2杂凑算法和相同的步骤创建第二个24字节响应;
(6) 服务器在数据库中查到同样的HASH值并对随机质询数作同样的运算,将所得与收到的应答码作比较。若匹配,则认证通过;
(7) 生成会话密钥用于MPPE加密。

 

MPPE (RFC3078、3079)

MPPE流量加密的大致流程如下:(具体见RFC3078、3079)
(1) 初始化会话密钥
(2) 生成RC4密钥
(3) 数据加密
(4) 密钥同步的2个模式

  • 无状态模式:每个包加密的密钥都是不同的, 每个包都要重新计算会话密钥,每个包都会设置“A”标志
  • 状态保持模式:发送方发现序号的后8位已经为0xff时更新密钥,更新完再加密和发送,包中设置“A”标志;

 

出题思路:

在建立好PPTP连接后,捕获完整流量。这里的密码可以根据流量包里的挑战值Challenge和响应值Response爆破得到。然后分析chap认证协议的实现代码,发现不需要用到挑战值,于是把这个包给去掉。最后按照MEEP协议解密流量。

此题主要是理解chap协议认证原理(RFC 2433或RFC 2759)以及MEEP协议(RFC3078、3079)加密原理,利用chap解出密码,再用MEEP还原流。

 

解题思路:

打开流量包基本是PPP Comp的流量,有个flag{fake_hint_weak_password}提示弱口令(用户名也提示了的)

流量是基于PPTP的VPN通信 ,采用了CHAP做单向认证,MPPE用于加密流量,参照附录的RFC文档编写程序

由于CHAP做认证时Password是加密过的,(提示是弱口令),尝试爆破。正常的解题思路是根据Challenge和Response爆破Password,而题目只有Response。但是由于捕获的流量没有CHAP的第一个包,也就没有挑战值(8个字节),爆破挑战值是不可能了。

分析RFC 2433或RFC 2759,需要根据协议的流程,找到 PasswordHash和Challenge生成Response的实现伪代码(也就是上面CHAP v1 协议流程的第5步),如下:

写成Python代码,具体实现函数如下:

函数大致功能是将hash处理后的Password分为3份,作为DES的Key给Challenge加3次密,Response保存3次加密的结果。编写逆程序,即利用PasswordHash和Response解密生成Challenge,代码如下:

这里会得到3个Challenge,如果都相等,则表示当前爆破的密码是对的。那么就可以以此为依据爆破密码,得到6位纯数字密码:729174。同时Challenge也可以得到:a0dc69227cde47db

再利用RFC的MPPE文档编写还原程序即可得到明文。

 

具体操作:

打开1.pcap,找到第3个包(CHAP认证的第二个响应包)

找到其中的响应值Response,Value Size为49,取Response=Value[24:48],Hex值如下:

在“exp-1密码爆破及挑战值还原.py”中填入Response

运行得到结果如下:

得到Password和Challenge之后就可以还原数据流得到flag,可以利用网上的工具。这里给出本人根据MPPE协议解密脚本(exp-2流还原.py和MSCHAP.py),供大家参考,便于理解协议细节。

 

附件链接

附件链接:https://pan.baidu.com/s/1L8cq8UJAT5aE-oezCQrwyQ
提取码:dltp

 

解题脚本

exp-1密码爆破及挑战值还原.py

import os
import string
import binascii
import hashlib
from binascii import b2a_hex, a2b_hex
from Crypto.Hash import MD4
from Crypto.Cipher import DES
from Crypto.Util.number import long_to_bytes, bytes_to_long

def md4(b):
    h = MD4.new()
    h.update(b)
    return h.digest()

def sha1(b):
    sha = hashlib.sha1(b)
    return sha.digest()

def NtPasswordHash(Password):
    md4 = MD4.new()
    md4.update(Password)
    pwhash = md4.hexdigest()
    return long_to_bytes(int(pwhash, 16))

def InsertBit(key):
    l = bytes_to_long(key)
    l = bin(l)[2:].zfill(56)
    l = list(l)
    l.insert(7, '0')
    l.insert(15, '0')
    l.insert(23, '0')
    l.insert(31, '0')
    l.insert(39, '0')
    l.insert(47, '0')
    l.insert(55, '0')
    l.insert(63, '0')
    res = "".join(l)
    res = long_to_bytes(int(res, 2))
    return res

def pad(PasswordHash):
    ZPasswordHash = PasswordHash +(21 - len(PasswordHash)) * b'\x00'
    return ZPasswordHash

def Password2Unicode(Password):
    Password_Unicode = ""
    for ch in Password:
        Password_Unicode += ch + "\x00"
    return Password_Unicode

def ChallengeResponse(Challenge, PasswordHash):
    ZPasswordHash = pad(PasswordHash)
    Response = b""
    for i in range(3):
        key = ZPasswordHash[i*7:i*7+7]
        key = InsertBit(key)
        des = DES.new(key, DES.MODE_ECB)
        Response += des.encrypt(Challenge)
        print(f"Response({len(Response)}): {b2a_hex(Response)}")
    return Response

def ResponseChallenge(Response, PasswordHash):
    ZPasswordHash = pad(PasswordHash)
    Challenge_list = []
    for i in range(3):
        key = ZPasswordHash[i*7:i*7+7]
        key = InsertBit(key)
        try:
            des = DES.new(key, DES.MODE_ECB)
        except:
            return None
        Challenge_list.append(des.decrypt(Response[i*8:i*8+8]))
    return Challenge_list

def jiami(Password, Challenge):
    Password_Unicode = Password2Unicode(Password)
    pwhash = NtPasswordHash(Password_Unicode.encode())
    s = ChallengeResponse(Challenge, pwhash)
    s = bytes_to_long(s)
    s = hex(s)[2:]
    return s

def get_Response(Password, Challenge):
    Challenge = a2b_hex(Challenge)
    response = jiami(Password, Challenge)
    # print("响应值:", response)
    return response

def jiemi(Password, Response):
    Password_Unicode = Password2Unicode(Password)
    pwhash = NtPasswordHash(Password_Unicode.encode())
    Challenge_list = ResponseChallenge(Response, pwhash)
    ''' 3次的挑战值相等,则爆破成功 '''
    if Challenge_list!=None and Challenge_list[0] == Challenge_list[1] and Challenge_list[0] == Challenge_list[2]:
        s = Challenge_list[0]
        s = bytes_to_long(s)
        s = hex(s)[2:]
        return s
    else:
        return None

def get_Challenge(Password, Response):
    Response = a2b_hex(Response)
    Challenge = jiemi(Password, Response)
    # print("挑战值:", Challenge)
    return Challenge

''' 生成长度为n,字符集为charset的字符串 '''
def generator(n, charset=string.digits+string.ascii_uppercase+string.ascii_lowercase):
    if n == 0:
        yield ''      ################
        return
    f = generator(n-1, charset)
    for s in f:
        for i in range(len(charset)):
            yield charset[i]+s

''' 生成长度不超过n,字符集为charset的字符串 '''
def Generator(n, charset=string.digits):
    global global_cnt
    for i in range(n+1):
        f = generator(i, charset=charset)
        for s in f:
            global_cnt += 1
            s = s[::-1]
            if global_cnt % 737 == 0:
                print(s, end='\r')
            yield s

if __name__ == '__main__':
    global_cnt = 0
    Response = "8a1e597d699574ff810dbc3798640fa584ccf9524857c45a"   ### 在这里填写 Response ###
    gen = Generator(10)         # 爆破字典
    for Password in gen:
        Challenge = get_Challenge(Password, Response)
        if Challenge != None:
            print("Password:", Password)
            print("Challenge:", Challenge)
            break

    ''' result '''
    # Password: 729174
    # Challenge: a0dc69227cde47db

    ''' test '''
    MyResponse = get_Response(Password, Challenge)
    print("响应值:", MyResponse, MyResponse==Response)
    # 响应值: 8a1e597d699574ff810dbc3798640fa584ccf9524857c45a (== Response)
    input()
    input()
    input()

exp-2流还原.py

import os
import re
import uuid
import base64
import binascii
from MSCHAP import *
from Crypto.Hash import MD4
from Crypto.Cipher import DES
from Crypto.Cipher import ARC4
from binascii import b2a_hex, a2b_hex
try:
    import scapy.all as scapy
except ImportError:
    import scapy

''' 2.4.  Key Derivation Functions '''
SHApad1 = b'\x00' * 40
SHApad2 = b'\xf2' * 40

def Get_Key(InitialSessionKey, CurrentSessionKey, LengthOfDesiredKey):
    Context = InitialSessionKey[0:LengthOfDesiredKey]
    Context += SHApad1
    Context += CurrentSessionKey[0:LengthOfDesiredKey]
    Context += SHApad2
    CurrentSessionKey = sha1(Context) 
    return CurrentSessionKey[0:LengthOfDesiredKey]

def Get_Start_Key(Challenge, NtPasswordHashHash):   # 8-octet, 16-octet
    InitialSessionKey = sha1(NtPasswordHashHash + NtPasswordHashHash + Challenge)
    return InitialSessionKey[:16]

def rc4_decrpt_hex(data, key):
    rc41=ARC4.new(key)
    # print dir(rc41)
    return rc41.decrypt(data)

def get_CurrentSessionKey(InitialSessionKey, CurrentSessionKey):
    CurrentSessionKey = Get_Key(InitialSessionKey, CurrentSessionKey, 16)
    CurrentSessionKey = rc4_decrpt_hex(CurrentSessionKey, CurrentSessionKey)
    return CurrentSessionKey

if __name__ == "__main__":
    # 0-to-256-unicode-char Password
    Password = "729174"                 ### 在这里填写 Password    ###
    # 8-octet Challenge
    Challenge = "a0dc69227cde47db"      ### 在这里填写 Challenge   ###

    ''' 1 '''
    pcap_path = '1.pcap'                   ### 在这里填写 pcap文件位置 ###

    '''  '''
    Challenge = bytes.fromhex(Challenge)
    PasswordHash = NtPasswordHash(Password)
    print('PasswordHash:', PasswordHash.hex())
    PasswordHashHash = HashNtPasswordHash(PasswordHash)
    print('PasswordHashHash:', PasswordHashHash.hex())

    ''' Generating 128-bit Session Keys '''
    # 初始密钥
    InitialSessionKey = Get_Start_Key(Challenge, PasswordHashHash)
    print('InitialSessionKey:', InitialSessionKey.hex())
    # 当前会话密钥
    CurrentSessionKey = InitialSessionKey
    print('CurrentSessionKey:', CurrentSessionKey.hex())
    CurrentSessionKey = Get_Key(InitialSessionKey, CurrentSessionKey, 16)
    print('CurrentSessionKey:', CurrentSessionKey.hex())

    ''' 抓包 '''
    pcap_cnt = 0
    comp_data_list = []
    packets = scapy.rdpcap(pcap_path)
    for packet in packets:
        pcap_cnt += 1
        if packet.haslayer('PPP_') and packet['IP'].src == '192.168.188.170':
            comp_data_list.append(bytes(packet[4])[2:])

    ''' 加密数据流解密 '''
    output = b''
    for j in range(len(comp_data_list)):
        # print('数据流', j)
        # 当前会话密钥(迭代)
        CurrentSessionKey = get_CurrentSessionKey(InitialSessionKey, CurrentSessionKey)
        # 当前加密数据
        data = comp_data_list[j]
        result = rc4_decrpt_hex(data, CurrentSessionKey)
        # print(result.hex())
        output += result
        # print()
    print('############################### 最终结果 ##################################################')
    # print(output)
    flag_Regex = re.compile(r'flag{.*?}')
    flag_results = flag_Regex.findall(output.decode('ISO8859-1'))
    print(flag_results)

MSCHAP.py

# 根据RFC2759文档编写
import os
import hashlib
from Crypto.Hash import MD4
from Crypto.Cipher import DES

def md4(b):
    h = MD4.new()
    h.update(b)
    return h.digest()

def sha1(b):
    sha = hashlib.sha1(b)
    return sha.digest()

def odd_even_parity(b): # 奇偶校验
    result = ''
    for i in range(0, len(b), 7):
        if b[i:i+7].count('1') % 2 == 0:
            result += b[i:i+7]+'0'
        else:
            result += b[i:i+7]+'1'
    return(result)

''' 8.1 '''
def  GenerateNTResponse(AuthenticatorChallenge, PeerChallenge, UserName, Password):
    Challenge =  ChallengeHash(PeerChallenge, AuthenticatorChallenge, UserName) # 8-octet
    PasswordHash = NtPasswordHash(Password)         # 16-octet
    NT_Response = ChallengeResponse(Challenge, PasswordHash)   # 24-octet
    return NT_Response     # 24-octet

''' 8.2 '''
def ChallengeHash(PeerChallenge, AuthenticatorChallenge, UserName):
    UserName = UserName.encode('utf8')
    Context = sha1(PeerChallenge+AuthenticatorChallenge+UserName)
    Challenge = Context[:8]
    return Challenge    # 8-octet 

''' 8.3 PasswordHash = NTLM_Hash(Password) '''
def NtPasswordHash(Password):
    # Password转换成Unicode编码(utf-16编码去掉前缀 FF FE)
    Bytes = Password.encode('utf16')[2:]
    # 对Unicode编码进行MD4加密
    PasswordHash = md4(Bytes)
    return PasswordHash # 16-octet

''' 8.4 PasswordHashHash = MD4(PasswordHash) '''
def HashNtPasswordHash(PasswordHash):   # 16-octet
    PasswordHashHash = md4(PasswordHash)
    return PasswordHashHash     # 16-octet

''' 8.5 '''
def ChallengeResponse(Challenge, PasswordHash):  # 8-octet, 16-octet
    ''' Step 1: 16字节PasswordHashHash分成3份 7 7 2'''
    part1 = PasswordHash[0:7]
    part2 = PasswordHash[7:14]
    part3 = PasswordHash[14:16]
    ''' Step 2: 奇偶校验+扩展 '''
    # part1 (每7bits+1bit校验位)7bytes==>8bytes
    Bits = bytes2bits(part1)
    Bits = odd_even_parity(Bits)
    key1 = bits2bytes(Bits)
    # part2 (每7bits+1bit校验位)7bytes==>8bytes
    Bits = bytes2bits(part2)
    Bits = odd_even_parity(Bits)
    key2 = bits2bytes(Bits)
    # part3 (先添5个字节的0,在每7bits+1bit校验位)2bytes==>7bytes==>8bytes
    Bits = bytes2bits(part3+b'\x00'*5)
    Bits = odd_even_parity(Bits)
    key3 = bits2bytes(Bits)
    ''' Step 3: DES3 '''
    result1 = DesEncrypt(Challenge, key1)[:8]
    result2 = DesEncrypt(Challenge, key2)[:8]
    result3 = DesEncrypt(Challenge, key3)[:8]
    Response = result1 + result2 + result3
    return Response     # 24-octet

''' 8.6 '''
def DesEncrypt(Clear, Key): # 8-octet, 7-octet
    if Clear is None:
        return ""
    # ECB方式
    generator = DES.new(Key, DES.MODE_ECB)
    # 非8整数倍明文补位
    pad = 8 - len(Clear) % 8
    pad_str = b""
    for i in range(pad):
        pad_str = pad_str + int.to_bytes(pad, length=1, byteorder='big')
    # 加密
    Cypher = generator.encrypt(Clear + pad_str)
    return Cypher       # 8-octet
(完)